Native Tabs
Native tabs provide platform-native bottom tab navigation using iOS UITabBar and Android Material BottomNavigation components via react-native-screens.
Native tabs are currently in preview. The API may change. Use expo-router/unstable-native-tabs.
Installation
Ensure you have the required dependencies:
npx expo install react-native-screens
Basic Native Tabs
Import from the unstable package:
import { NativeTabs } from 'expo-router/unstable-native-tabs' ;
export default function TabLayout () {
return (
< NativeTabs >
< NativeTabs.Trigger name = "home" />
< NativeTabs.Trigger name = "explore" />
< NativeTabs.Trigger name = "profile" />
</ NativeTabs >
);
}
From the source (NativeTabs.tsx:9-36):
/**
* The component used to create native tabs layout.
*
* @example
* import { NativeTabs } from 'expo-router/unstable-native-tabs';
*
* export default function Layout() {
* return (
* <NativeTabs>
* <NativeTabs.Trigger name="home" />
* <NativeTabs.Trigger name="settings" />
* </NativeTabs>
* );
* }
*/
export const NativeTabs = Object . assign (
( props : NativeTabsProps ) => {
return < NativeTabsNavigatorWrapper { ... props } />;
},
{ Trigger: NativeTabTrigger , BottomAccessory }
);
From CLAUDE.md:241-253:
### Native Tabs
Native tabs provide native bottom tab navigation using iOS UITabBar
and Android Material BottomNavigation via `react-native-screens`.
- `NativeTabs` - Layout component using `useNavigationBuilder`
- `NativeTabs.Trigger` - Tab configuration (icon, label, badge)
- `NativeTabs.BottomAccessory` - iOS 26+ bottom accessory
- Icons: `sf` (SF Symbols), `drawable` (Android), `md` (Material), `src` (images)
- iOS-specific: blur effects, transparency, sidebar adaptable
- Android-specific: Material Design 3, dynamic colors, ripple effects
With Labels and Icons
import { NativeTabs } from 'expo-router/unstable-native-tabs' ;
export default function TabLayout () {
return (
< NativeTabs >
< NativeTabs.Trigger name = "home" >
< NativeTabs.Trigger.Label > Home </ NativeTabs.Trigger.Label >
< NativeTabs.Trigger.Icon platform = "ios" name = "house" />
< NativeTabs.Trigger.Icon platform = "android" name = "home" />
</ NativeTabs.Trigger >
< NativeTabs.Trigger name = "explore" >
< NativeTabs.Trigger.Label > Explore </ NativeTabs.Trigger.Label >
< NativeTabs.Trigger.Icon platform = "ios" name = "safari" />
< NativeTabs.Trigger.Icon platform = "android" name = "explore" />
</ NativeTabs.Trigger >
< NativeTabs.Trigger name = "profile" >
< NativeTabs.Trigger.Label > Profile </ NativeTabs.Trigger.Label >
< NativeTabs.Trigger.Icon platform = "ios" name = "person" />
< NativeTabs.Trigger.Icon platform = "android" name = "person" />
</ NativeTabs.Trigger >
</ NativeTabs >
);
}
Icon Types
SF Symbols (iOS)
< NativeTabs.Trigger.Icon
platform = "ios"
type = "sf"
name = "house.fill"
/>
Material Icons (Android)
< NativeTabs.Trigger.Icon
platform = "android"
type = "md"
name = "home"
/>
Android Drawables
< NativeTabs.Trigger.Icon
platform = "android"
type = "drawable"
name = "ic_home"
/>
Image Source
< NativeTabs.Trigger.Icon
type = "src"
source = { require ( './home-icon.png' ) }
/>
Badges
Add notification badges:
< NativeTabs.Trigger name = "notifications" >
< NativeTabs.Trigger.Label > Notifications </ NativeTabs.Trigger.Label >
< NativeTabs.Trigger.Icon platform = "ios" name = "bell" />
< NativeTabs.Trigger.Badge > 5 </ NativeTabs.Trigger.Badge >
</ NativeTabs.Trigger >
With dynamic count:
export default function TabLayout () {
const [ unreadCount , setUnreadCount ] = useState ( 0 );
return (
< NativeTabs >
< NativeTabs.Trigger name = "messages" >
< NativeTabs.Trigger.Label > Messages </ NativeTabs.Trigger.Label >
< NativeTabs.Trigger.Icon platform = "ios" name = "message" />
{ unreadCount > 0 && (
< NativeTabs.Trigger.Badge > { unreadCount } </ NativeTabs.Trigger.Badge >
) }
</ NativeTabs.Trigger >
</ NativeTabs >
);
}
iOS-Specific Features
Blur Effects
< NativeTabs
appearance = { {
ios: {
style: 'defaultBlur' , // or 'opaqueBlur', 'transparent'
},
} }
/>
< NativeTabs
appearance = { {
ios: {
scrollEdgeAppearance: {
style: 'opaqueBlur' ,
},
},
} }
/>
< NativeTabs
appearance = { {
ios: {
sidebarAdaptable: true ,
},
} }
/>
Android-Specific Features
Material Design 3
< NativeTabs
appearance = { {
android: {
elevation: 8 ,
rippleColor: '#f4511e' ,
},
} }
/>
Dynamic Colors
< NativeTabs
appearance = { {
android: {
useDynamicColors: true ,
},
} }
/>
Indicator Style
< NativeTabs
appearance = { {
android: {
indicatorStyle: 'pill' , // or 'line'
},
} }
/>
Bottom Accessory (iOS 26+)
Add accessory content below tabs:
import { NativeTabs } from 'expo-router/unstable-native-tabs' ;
export default function TabLayout () {
const placement = NativeTabs . BottomAccessory . usePlacement ();
return (
< NativeTabs >
< NativeTabs.Trigger name = "home" />
< NativeTabs.Trigger name = "profile" />
< NativeTabs.BottomAccessory >
< View style = { { padding: 16 , backgroundColor: '#f0f0f0' } } >
< Text > Accessory Content </ Text >
< Text > Placement: { placement } </ Text >
</ View >
</ NativeTabs.BottomAccessory >
</ NativeTabs >
);
}
Hide Tabs
Hide tabs on specific screens:
import { NativeTabs } from 'expo-router/unstable-native-tabs' ;
export default function Details () {
return (
<>
< NativeTabs.Screen options = { { tabBarHidden: true } } />
< View >
< Text > Details Screen </ Text >
</ View >
</>
);
}
Nested Navigation
Combine with Stack:
app/
(tabs)/
_layout.tsx
home/
_layout.tsx # Stack
index.tsx
details.tsx
app/(tabs)/home/_layout.tsx
import { Stack } from 'expo-router' ;
export default function HomeStack () {
return < Stack /> ;
}
Show different tabs per platform:
import { Platform } from 'react-native' ;
import { NativeTabs } from 'expo-router/unstable-native-tabs' ;
export default function TabLayout () {
return (
< NativeTabs >
< NativeTabs.Trigger name = "home" >
< NativeTabs.Trigger.Icon
platform = "ios"
name = "house.fill"
/>
< NativeTabs.Trigger.Icon
platform = "android"
name = "home"
/>
</ NativeTabs.Trigger >
{ Platform . OS === 'ios' && (
< NativeTabs.Trigger name = "ios-only" >
< NativeTabs.Trigger.Label > iOS Only </ NativeTabs.Trigger.Label >
</ NativeTabs.Trigger >
) }
</ NativeTabs >
);
}
Tab Configuration
Initial Tab
export const unstable_settings = {
initialRouteName: 'home' ,
};
export default function TabLayout () {
return (
< NativeTabs >
< NativeTabs.Trigger name = "home" />
< NativeTabs.Trigger name = "profile" />
</ NativeTabs >
);
}
Tab Order
Tabs appear in the order they’re defined:
< NativeTabs >
< NativeTabs.Trigger name = "first" /> { /* Left/Top */ }
< NativeTabs.Trigger name = "second" />
< NativeTabs.Trigger name = "third" /> { /* Right/Bottom */ }
</ NativeTabs >
Common Patterns
Conditional Tab Badge
export default function TabLayout () {
const { hasUpdates } = useNotifications ();
return (
< NativeTabs >
< NativeTabs.Trigger name = "updates" >
< NativeTabs.Trigger.Label > Updates </ NativeTabs.Trigger.Label >
< NativeTabs.Trigger.Icon platform = "ios" name = "bell" />
{ hasUpdates && (
< NativeTabs.Trigger.Badge />
) }
</ NativeTabs.Trigger >
</ NativeTabs >
);
}
Dynamic Icon
export default function TabLayout () {
const { isOnline } = useConnection ();
return (
< NativeTabs >
< NativeTabs.Trigger name = "status" >
< NativeTabs.Trigger.Label > Status </ NativeTabs.Trigger.Label >
< NativeTabs.Trigger.Icon
platform = "ios"
name = { isOnline ? 'wifi' : 'wifi.slash' }
/>
</ NativeTabs.Trigger >
</ NativeTabs >
);
}
Theme-Based Appearance
export default function TabLayout () {
const { theme } = useTheme ();
return (
< NativeTabs
appearance = { {
ios: {
style: theme === 'dark' ? 'opaqueBlur' : 'defaultBlur' ,
},
android: {
useDynamicColors: theme === 'auto' ,
},
} }
>
< NativeTabs.Trigger name = "home" />
</ NativeTabs >
);
}
Migration from Tabs
Before (Regular Tabs)
import { Tabs } from 'expo-router' ;
export default function TabLayout () {
return (
< Tabs >
< Tabs.Screen
name = "home"
options = { {
tabBarIcon : ({ color }) => < Icon name = "home" color = { color } /> ,
} }
/>
</ Tabs >
);
}
After (Native Tabs)
import { NativeTabs } from 'expo-router/unstable-native-tabs' ;
export default function TabLayout () {
return (
< NativeTabs >
< NativeTabs.Trigger name = "home" >
< NativeTabs.Trigger.Icon platform = "ios" name = "house" />
< NativeTabs.Trigger.Icon platform = "android" name = "home" />
</ NativeTabs.Trigger >
</ NativeTabs >
);
}
Best Practices
Use Native Icons
// Good: Platform-native icons
< NativeTabs.Trigger.Icon platform = "ios" type = "sf" name = "house" />
< NativeTabs.Trigger.Icon platform = "android" type = "md" name = "home" />
// Avoid: Custom images when native icons exist
< NativeTabs.Trigger.Icon type = "src" source = { require ( './home.png' ) } />
// Good: Optimized for each platform
< NativeTabs.Trigger name = "home" >
< NativeTabs.Trigger.Icon platform = "ios" name = "house.fill" />
< NativeTabs.Trigger.Icon platform = "android" name = "home" />
</ NativeTabs.Trigger >
// Avoid: Single icon for all platforms
< NativeTabs.Trigger name = "home" >
< NativeTabs.Trigger.Icon type = "src" source = { require ( './icon.png' ) } />
</ NativeTabs.Trigger >
Keep Tab Count Reasonable
// Good: 3-5 tabs
< NativeTabs >
< NativeTabs.Trigger name = "home" />
< NativeTabs.Trigger name = "search" />
< NativeTabs.Trigger name = "profile" />
</ NativeTabs >
// Avoid: Too many tabs
< NativeTabs >
{ /* 6+ tabs */ }
</ NativeTabs >
Limitations
Currently in preview - API may change
Requires react-native-screens 4.0+
Some features require iOS 26+ or Android 12+
Bottom accessory only works on iOS 26+
Sidebar adaptable only on iPadOS 18+
Next Steps
Tab Navigation Compare with regular tabs
Stack Navigation Combine with stacks
Layouts Understand layouts
Navigation Navigate between tabs