expo-router
Navigation and routing with Expo Router: file-based and dynamic routes, Link previews and context menus, native Stack, modals and form sheets, NativeTabs, headers, toolbars and search bars.
Install
npx skills add https://github.com/expo/skills --skill expo-routerNative Tabs
Always prefer NativeTabs from 'expo-router/unstable-native-tabs' for the best iOS experience.
SDK 54+. SDK 55 recommended.
SDK Compatibility
All examples below use SDK 55 syntax. For SDK 54, replace NativeTabs.Trigger.Icon/Label/Badge with standalone Icon, Label, Badge imports.
Basic Usage
Rules
- You must include a trigger for each tab
- The
NativeTabs.Trigger'name' must match the route name, including parentheses (e.g.<NativeTabs.Trigger name="(search)">) - Prefer search tab to be last in the list so it can combine with the search bar
- Use the 'role' prop for common tab types
- Tabs must be static — no dynamic addition/removal at runtime (remounts navigator, loses state)
Platform Features
Native Tabs use platform-specific tab bar implementations:
- iOS 26+: Liquid glass effects with system-native appearance
- Android: Material 3 bottom navigation
- Better performance and native feel
Icon Component
renderingMode: "template" applies tint color (single-color icons), "original" preserves source colors (gradients). Android always uses original.
Label & Badge
iOS 26 Features
Liquid Glass Tab Bar
The tab bar automatically adopts liquid glass appearance on iOS 26+.
Minimize on Scroll
Search Tab
Note: Place search tab last for best UX.
Role Prop
Use semantic roles for special tab types:
Available roles: search | more | favorites | bookmarks | contacts | downloads | featured | history | mostRecent | mostViewed | recents | topRated
Customization
Tint Color
Dynamic Colors (iOS)
Use DynamicColorIOS for colors that adapt to liquid glass:
Conditional Tabs
Don't hide the tabs when they are visible - toggling visibility remounts the navigator; Do it only during the initial render.
Note: Hidden tabs cannot be navigated to!
Behavior Options
Hidden Tab Bar (SDK 55+)
Use hidden prop on NativeTabs to hide the entire tab bar dynamically:
Bottom Accessory (SDK 55+)
NativeTabs.BottomAccessory renders content above the tab bar (iOS 26+). Uses usePlacement() to adapt between 'regular' and 'inline' layouts.
Important: Two instances render simultaneously — store state outside the component (props, context, or external store).
Safe Area Handling (SDK 55+)
SDK 55 handles safe areas automatically:
- Android: Content wrapped in SafeAreaView (bottom inset)
- iOS: First ScrollView gets automatic
contentInsetAdjustmentBehavior
To opt out per-tab, use disableAutomaticContentInsets and manage manually:
Using Vector Icons
If you must use @expo/vector-icons instead of SF Symbols:
Prefer SF Symbols + md prop over vector icons for native feel.
If you are using SDK 55 and later use the md prop to specify Material Symbols used on Android.
Structure with Stacks
Native tabs don't render headers. Nest Stacks inside each tab for navigation headers:
Custom Web Layout
Use platform-specific files for separate native and web tab layouts:
Or extract to a component: components/app-tabs.tsx + components/app-tabs.web.tsx.
Migration from JS Tabs
Before (JS Tabs)
After (Native Tabs)
Key Differences
Limitations
- Android: Maximum 5 tabs (Material Design constraint)
- Nesting: Native tabs cannot nest inside other native tabs
- Tab bar height: Cannot be measured programmatically
- FlatList transparency: Use
disableTransparentOnScrollEdgeto fix issues - Dynamic tabs: Tabs must be static; changes remount navigator and lose state
Keyboard Handling (Android)
Configure in app.json:
Common Issues
- Icons not showing on Android: Add
mdprop (SDK 55) or use VectorIcon - Headers missing: Nest a Stack inside each tab group
- Trigger name mismatch:
namemust match exact route name including parentheses - Badge not visible: Badge must be a child of Trigger, not a prop
- Tab bar transparent on iOS 18 and earlier: If the screen uses a
ScrollVieworFlatList, make sure it is the first opaque child of the screen component. If it needs to be wrapped in anotherView, ensure the wrapper usescollapsable={false}. If the screen does not use aScrollVieworFlatList, setdisableTransparentOnScrollEdgetotruein theNativeTabs.Triggeroptions, to make the tab bar opaque. - Scroll to top not working: Ensure
disableScrollToTopis not set on the active tab's Trigger andScrollViewis the first child of the screen component. - Header buttons flicker when navigating between tabs: Make sure the app is wrapped in a
ThemeProvider
If the app only uses a light or dark theme, you can directly pass DarkTheme or DefaultTheme to ThemeProvider without checking the color scheme.