Back to Articles
Native Tabs in Expo Router: The iOS 26 Liquid Glass Era

Native Tabs in Expo Router: The iOS 26 Liquid Glass Era

Jan 18, 2026•3 min read
tutorialExpoReact NativeExpo RouteriOS
Share

Why Native Tabs Matter

If you've ever tried to perfectly replicate the iOS tab bar in React Native, you know the pain. The blur effects, the animations, the dynamic island interactions. JavaScript just can't match native behavior.

With SDK 54+, Expo Router introduced NativeTabs. Unlike the JavaScript <Tabs /> component, this renders a real native tab bar. On iOS, you get the system UITabBarController. On Android, Material Tabs.

And with iOS 26, Apple introduced "Liquid Glass", the translucent, context-aware UI layer that adapts to your content. Native tabs get this for free. This guide is based on the official Expo documentation.

Getting Started

First, make sure you're on Expo SDK 54 or later. The API lives in an unstable import (for now):

tsx
import { NativeTabs } from 'expo-router/unstable-native-tabs';

Here's the minimal file structure:

text
app/ ├── _layout.tsx ├── index.tsx └── settings.tsx

And the layout file:

tsx
// app/_layout.tsx import { NativeTabs } from 'expo-router/unstable-native-tabs'; export default function TabLayout() { return ( <NativeTabs> <NativeTabs.Trigger name="index"> <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label> <NativeTabs.Trigger.Icon sf="house.fill" md="home" /> </NativeTabs.Trigger> <NativeTabs.Trigger name="settings"> <NativeTabs.Trigger.Icon sf="gear" md="settings" /> <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label> </NativeTabs.Trigger> </NativeTabs> ); }

That's it. Two tabs, native rendering, SF Symbols on iOS, Material icons on Android.

The Trigger API

Unlike JavaScript tabs where routes are automatically added, NativeTabs uses Triggers. You explicitly declare which routes appear in the tab bar.

This gives you control:

tsx
// Hide a tab conditionally <NativeTabs.Trigger name="admin" hidden={!isAdmin} />

Note: Tabs aren't added automatically. If a route exists but has no Trigger, it won't appear in the tab bar.

Icons: SF Symbols, Material, and Custom

The Icon component is flexible. You can use:

Platform Symbols

tsx
<NativeTabs.Trigger.Icon sf="house.fill" // iOS SF Symbol md="home" // Android Material icon />

Selected/Unselected States (iOS)

tsx
<NativeTabs.Trigger.Icon sf={{ default: 'house', selected: 'house.fill' }} md="home" />

Custom Images

tsx
<NativeTabs.Trigger.Icon src={require('../assets/custom-icon.png')} />

For multi-color icons, set renderingMode="original" to preserve colors instead of applying the tint.

Badges

Adding notification badges is straightforward:

tsx
<NativeTabs.Trigger name="messages"> <NativeTabs.Trigger.Badge>9+</NativeTabs.Trigger.Badge> <NativeTabs.Trigger.Label>Messages</NativeTabs.Trigger.Label> </NativeTabs.Trigger>

Pass no children for a simple dot indicator:

tsx
<NativeTabs.Trigger.Badge /> {/* Just a dot */}

Hiding the Tab Bar Dynamically

Sometimes you need to hide the tab bar for specific screens (video players, immersive content). Use a Context:

tsx
// context/TabBarContext.tsx import { createContext } from 'react'; export const TabBarContext = createContext<{ setIsTabBarHidden: (hidden: boolean) => void; }>({ setIsTabBarHidden: () => {} });
tsx
// app/_layout.tsx import { useState } from 'react'; import { NativeTabs } from 'expo-router/unstable-native-tabs'; import { TabBarContext } from '../context/TabBarContext'; export default function TabLayout() { const [isTabBarHidden, setIsTabBarHidden] = useState(false); return ( <TabBarContext value={{ setIsTabBarHidden }}> <NativeTabs hidden={isTabBarHidden}> {/* triggers */} </NativeTabs> </TabBarContext> ); }

Then in any screen:

tsx
import { useFocusEffect } from 'expo-router'; import { use } from 'react'; import { TabBarContext } from '../context/TabBarContext'; export default function VideoPlayer() { const { setIsTabBarHidden } = use(TabBarContext); useFocusEffect(() => { setIsTabBarHidden(true); return () => setIsTabBarHidden(false); }); return <Video />; }

iOS 26 Features

If you're compiling with Xcode 26+, you get access to new features:

Separate Search Tab

tsx
<NativeTabs.Trigger name="search" role="search"> <NativeTabs.Trigger.Label>Search</NativeTabs.Trigger.Label> </NativeTabs.Trigger>

Minimize on Scroll

The tab bar can shrink when scrolling:

tsx
<NativeTabs minimizeBehavior="onScrollDown"> {/* triggers */} </NativeTabs>

Bottom Accessory (Mini Player)

For persistent controls like a music player:

tsx
<NativeTabs> <NativeTabs.BottomAccessory> <MiniPlayer /> </NativeTabs.BottomAccessory> {/* triggers */} </NativeTabs>

Common Gotchas

Transparent Tab Bar on iOS 18

If your tab bar becomes transparent when scrolling, add:

tsx
<NativeTabs.Trigger name="index" disableTransparentOnScrollEdge>

White Flash When Switching Tabs (iOS 26)

Wrap your app in a ThemeProvider:

tsx
import { ThemeProvider, DarkTheme } from '@react-navigation/native'; <ThemeProvider value={DarkTheme}> <NativeTabs>{/* tabs */}</NativeTabs> </ThemeProvider>

Scroll-to-Top Not Working

Make sure ScrollView is the first child of your screen, or set collapsable={false} on wrapper Views.

Migration from JavaScript Tabs

JavaScript TabsNative Tabs
Tabs.Screen componentNativeTabs.Trigger component
options prop with tabBarIconNativeTabs.Trigger.Icon child
Automatic route discoveryExplicit Trigger declaration
Full customizationPlatform-constrained

Known Limitations

  • 5 tab maximum on Android (Material constraint)
  • No nested native tabs (use JS tabs for nesting)
  • FlatList issues with scroll detection
  • No dynamic tab add/remove (tabs should be static)

Native tabs aren't a drop-in replacement. They're a paradigm shift toward trusting the platform. If you want buttery-smooth, system-native navigation that automatically benefits from OS updates like Liquid Glass, this is the path forward.

For more customization, stick with JavaScript tabs. For native polish, go native.

Related Articles

Installing a Senior React Native Engineer into My IDE: A Guide to Agent Skills

Installing a Senior React Native Engineer into My IDE: A Guide to Agent Skills

Replacing StackOverflow with Agent Skills

Replacing StackOverflow with Agent Skills

Why I Chosen Supabase Over Firebase for PaceFyndr

Why I Chosen Supabase Over Firebase for PaceFyndr