The iOS Liquid Glass Tab Bar Finally Came to Expo (And It's Better Than I Expected)

By amillionmonkeys
#Expo#React Native#iOS#Mobile Development

Expo SDK 54's new NativeTabs API brings true iOS liquid glass effects to React Native apps. Here's what I learned rebuilding a Pinterest clone with it.

I've been chasing the perfect iOS tab bar in React Native for years. You know the effect—that signature Apple translucent blur where content flows behind the tabs, creating a "liquid glass" appearance. It's everywhere in iOS: Messages, Photos, App Store. Native iOS developers get it for free with UITabBarController. The rest of us have been stuck with hacky workarounds.

Until Expo SDK 54 dropped the expo-router/unstable-native-tabs API.

I just finished rebuilding Pinx (a Pinterest client app) using this new feature, and I'm genuinely impressed. Here's what actually works, what breaks, and whether you should use it in production.

The Liquid Glass Problem Nobody Could Solve

If you've built React Native apps for iOS, you've probably hit this. The standard <Tabs> component from expo-router gives you a tab bar. It functions. But it looks... web-y. Flat. Opaque. Nothing like the translucent, blurred bars users expect from iOS apps.

I've tried everything over the years:

  • Custom blur components (performance nightmare)
  • Third-party tab bar libraries (never quite right)
  • BlurView hacks (close, but broke on every iOS update)
  • CSS backdrop-filter experiments (doesn't work in React Native)

The fundamental issue: React Native renders JavaScript views. The liquid glass effect requires native rendering with platform-specific blur APIs. You can't fake it with web technologies—at least not without tanking performance or breaking dark mode support.

What Expo SDK 54 Actually Ships

The new NativeTabs component doesn't try to recreate iOS's tab bar. It literally uses UITabBarController—Apple's actual native component.

This means:

  • Real translucent blur effect (for free)
  • Proper dark mode support (for free)
  • Native animations (for free)
  • Accessibility features (for free)
  • Zero custom styling needed

The effect in action is striking—content flows smoothly behind the translucent tab bar, the bar automatically minimizes when you scroll down through feeds or search results, and everything animates with that signature iOS fluidity you can't fake with JavaScript.

Here's the key insight from Expo's team: stop fighting the platform. Use the native components iOS provides, and expose them through a React API.

The Platform-Specific Approach (And Why It Works)

Here's how I implemented tabs in Pinx. The trick: different components for iOS and Android.

import { Tabs } from 'expo-router';
import { NativeTabs, Icon, Label } from 'expo-router/unstable-native-tabs';
import { Platform, PlatformColor } from 'react-native';
 
export default function TabLayout() {
  const { theme } = useTheme();
 
  // Android: traditional Tabs component
  if(Platform.OS === 'android') {
    return (
      <Tabs
        screenOptions={{
          tabBarActiveTintColor: PINTEREST_RED,
          tabBarInactiveTintColor: theme.colors.textMuted,
          tabBarStyle: {
            backgroundColor: theme.colors.surface,
            borderTopColor: theme.colors.border,
          },
          headerShown: false,
        }}
      >
        <Tabs.Screen
          name="index"
          options={{
            title: 'Boards',
            tabBarIcon: ({ color, focused }) => (
              <Ionicons name={focused ? 'grid' : 'grid-outline'} size={24} color={color} />
            )
          }}
        />
        {/* More tabs... */}
      </Tabs>
    );
  }
 
  // iOS: new NativeTabs component
  return (
    <NativeTabs
      minimizeBehavior="onScrollDown"
      iconColor={PlatformColor('systemRedColor')}
      tintColor={PlatformColor('systemRedColor')}
    >
      <NativeTabs.Trigger name="index">
        <Label>Boards</Label>
        <Icon sf="square.grid.2x2.fill" />
      </NativeTabs.Trigger>
 
      <NativeTabs.Trigger name="pins">
        <Label>Pins</Label>
        <Icon sf="pin.fill" />
      </NativeTabs.Trigger>
 
      <NativeTabs.Trigger name="settings" disableScrollToTop>
        <Label>Settings</Label>
        <Icon sf="gear" />
      </NativeTabs.Trigger>
 
      <NativeTabs.Trigger name="search" role="search">
        <Label>Search</Label>
      </NativeTabs.Trigger>
    </NativeTabs>
  );
}

This feels weird at first. Two completely different implementations for one feature? But it's the right call.

The documentation mentions NativeTabs works on Android too, but I found it buggy and it doesn't gain you much. Android's Material Design uses solid tab bars—there's no liquid glass effect to chase. I tried using NativeTabs on Android for consistency, hit weird rendering issues, and reverted to standard <Tabs>. Save yourself the trouble and use platform-specific components from the start.

The Magic of SF Symbols Integration

One of my favorite details: native SF Symbols support.

<Icon sf="square.grid.2x2.fill" />

That sf prop takes an SF Symbol name. No importing icon libraries. No bundling thousands of icon assets. You get access to Apple's entire SF Symbols catalog—1,200+ icons that automatically match iOS system weight, render at correct sizes, and support dynamic type.

For Android, you'd use the drawable prop to reference Android drawables. Platform-specific, but that's the point.

The Props That Actually Matter

minimizeBehavior

<NativeTabs minimizeBehavior="onScrollDown">

This hides the tab bar when users scroll down content. It's what Apple Music and Photos do. Users get more screen space, but tabs reappear on scroll up. Native iOS behavior, zero code from you.

The effect is subtle but polished—watch how the tab bar smoothly slides away as you scroll through boards or search results, then reappears when you reverse direction. It's the kind of detail that iOS users notice subconsciously.

PlatformColor for Tinting

tintColor={PlatformColor('systemRedColor')}

Don't hardcode color values. PlatformColor uses iOS's semantic color system. This means:

  • Colors adapt to light/dark mode automatically
  • High contrast mode support
  • Future iOS theme support

I used systemRedColor to match Pinterest's brand. Changed automatically between #FF3B30 (light) and #FF453A (dark).

disableScrollToTop

<NativeTabs.Trigger name="settings" disableScrollToTop>

iOS users expect tapping the active tab to scroll to top. It's muscle memory from Safari, Twitter, every iOS app. But sometimes you don't want that (like on a settings screen with no scroll view).

This prop lets you disable it per-tab. Small detail, but feels wrong when missing.

role="search"

<NativeTabs.Trigger name="search" role="search">

Tells iOS this tab triggers search. On devices with hardware keyboards, Command+F will now switch to this tab. Another tiny native behavior users expect.

What Actually Broke During Migration

I migrated Pinx from standard <Tabs> to <NativeTabs> over a weekend. A few gotchas:

1. Icon Libraries Don't Work the Same Way

With standard tabs, I used @expo/vector-icons:

tabBarIcon: ({ color }) => <Ionicons name="grid" size={24} color={color} />

With NativeTabs, you need SF Symbols or custom drawables. I ended up keeping @expo/vector-icons for Android and using SF Symbols for iOS. Platform-specific again.

2. Don't Bother with NativeTabs on Android

The docs say it works on Android. Technically true. But I tried it and hit rendering bugs with tab icons not showing consistently. Since Android doesn't get the liquid glass effect anyway (Material Design uses solid surfaces), there's no benefit.

Stick with standard <Tabs> for Android. Use <NativeTabs> only for iOS where it actually provides value.

3. The API is Actually "Unstable"

It's in expo-router/unstable-native-tabs for a reason. This is experimental. I hit one crash during development when mixing <Tabs> and <NativeTabs> in the same router tree. Don't do that.

The API will probably change before it's marked stable. Expo's good about migrations, but expect some churn if you adopt this early.

4. Theme State Can Get Weird

My theme context updates when users toggle dark mode. With standard tabs, theme changes re-rendered instantly. With NativeTabs, I occasionally saw a frame delay while UITabBarController updated.

Not a dealbreaker, but noticeable during development. Production users won't care—theme changes are rare.

5. No Custom Tab Bar Components

With standard tabs, you can pass tabBar props to render fully custom tab bars. NativeTabs doesn't support this. You get Apple's tab bar or nothing.

For 99% of apps, that's perfect. If you need wild custom tab designs, stick with standard tabs.

Performance: The Real Win

The liquid glass effect isn't just prettier. It's faster.

With blur hacks, I was rendering blur views in JavaScript, recalculating blur regions on scroll, and tanking frame rates anytime content moved behind tabs. Pinx's home feed (infinite scroll of images) would drop to 45fps with custom blur implementations.

NativeTabs renders the blur in native code on the GPU. The home feed now holds 60fps with the tab bar visible. Smooth scrolling with zero blur calculation overhead.

For image-heavy apps like Pinx, this matters. Users notice jank during scroll.

Should You Use This in Production?

Depends. Here's my take:

Yes, if:

  • You're building an iOS-focused app (or platform-specific versions)
  • You want truly native iOS feel without custom code
  • You're okay with experimental APIs (and future migrations)
  • You don't need complex custom tab bar designs

No, if:

  • You need identical UI on iOS and Android
  • You're shipping to clients who can't tolerate API churn
  • You need custom tab bar components with wild designs
  • You're on an older Expo SDK (this needs SDK 54+)

For Pinx, the "unstable" label scared me at first. But the improvement in feel and performance was worth it. iOS users notice the difference—several testers mentioned it felt "more like a real app" after the migration.

The Bigger Picture: Native UI in React Native

This API represents something bigger than tab bars. Expo's embracing native components over recreating native patterns in JavaScript.

We've spent years building libraries that try to mimic iOS and Android UIs in React Native. Most fall short. They're close, but never quite right—animations slightly off, blur effects fake, interactions missing subtle native behaviors.

The new approach: expose actual platform components through React APIs. Use UITabBarController on iOS. Use Material tabs on Android. Stop fighting platform patterns.

I expect more of this. Native navigation bars. Native modals. Native gestures. All exposed through unified React APIs that feel native because they are native.

What I'd Tell My Past Self

If I was starting Pinx today, I'd use NativeTabs from day one—despite the "unstable" label. The payoff in user experience and development time saved is worth the migration risk.

Three lessons from this rebuild:

  1. Platform-specific isn't always bad. Sometimes the right answer is different implementations for different platforms. Users don't care if your code is unified—they care if the app feels native.

  2. Don't fight the platform APIs. I spent years trying to recreate iOS blur in JavaScript. Two lines of NativeTabs code did it better.

  3. "Unstable" doesn't mean broken. Expo's experimental APIs are often production-ready with the caveat that naming or props might change. If you can handle migration costs, early adoption gets you better UX sooner.

The liquid glass tab bar is a small detail. But small details are what make iOS apps feel polished. After years of fighting for this effect, it's nice to finally have it work properly.

Building an iOS app with Expo? We've migrated a handful of React Native apps to SDK 54 and can help navigate the new features (and break-ables). Get in touch if you want someone who's already hit the gotchas.

T: 07512 944360 | E: [email protected]

© 2025 amillionmonkeys ltd. All rights reserved.