Blog · Engineering
Native tabs in Expo Router: the gotchas
Switching a React Native app’s tab bar to Expo Router’s native tabs is the biggest visible upgrade for the least code: a real UITabBarController on iPhone, which on iOS 26 means the Liquid Glass bar that floats and minimises, and a real Material bar on Android. The component is one import. The afternoon goes on four things the docs do not say loudly enough.
1. What you get, by platform
Since SDK 54 the tabs come from expo-router/unstable-native-tabs: a NativeTabs with a Trigger per tab, each with a label and an icon (an SF Symbol on iOS, a Material icon on Android). On iOS 26 the bar is the system’s: glass, floating, with minimizeBehavior="onScrollDown" to shrink it as the user scrolls. On Android it renders the platform’s Material tab bar. One component, two native bars, no JavaScript drawing either.
A stable import path is planned for SDK 58. Until then, the unstable- in the name is a promise that props may move between SDKs, so pin your SDK and read the changelog when you bump.
2. The minimising bar needs the ScrollView first
This is the one that eats the afternoon. minimizeBehavior only fires when the first descendant of the tab’s screen is the scrolling view (ScrollView, FlatList, SectionList). Wrap the list in a View for a header and the bar never moves.
People look at contentInsetAdjustmentBehavior first, because that is where scroll-related iOS behaviour usually lives. It is unrelated: the bar minimises with contentInsetAdjustmentBehavior="never" too, as long as the scroll view comes first. Put the list first and render headers inside it (ListHeaderComponent), or as absolutely positioned overlays.
3. Only the launch tab gets the top inset for free
The tab the app opens on has its content pushed below the status bar and header automatically. Switch to a second tab and its content starts under the status bar. It is easy to miss on the simulator because you test the first tab most.
Apply the top safe-area inset yourself on every tab’s first screen, the same way, and test by launching into each tab in turn.
4. Android stops at five
The Material bar allows at most five tabs, and the component enforces it. This is a design limit, not a bug: Apple’s bar handles more by folding them into “More”, Android’s does not. Keep tabs to five on both and the two platforms stay in step. If a sixth is tempting, it belongs on a screen or under Profile.
5. Test the swipe on the simulator, scripted
Minimising only shows when you scroll, and nobody wants to swipe by hand after every change. The iOS simulator’s swipes can be scripted (the xcrun simctl tools and an accessibility driver will drive a real gesture), so the check “bar minimises on the Diary tab, restores on scroll up” can run on every build. The same script catches the inset mistake in section 3, because a screenshot of each tab after launch shows content under the status bar at once.
Where to start
Move the tab bar to native tabs, launch into each tab and screenshot it, then scroll the main list and watch the bar. If it does not move, your scroll view is not first.
An app from the App Builder ships with native tabs wired this way: the scroll view first on every screen, the inset applied on every tab, five tabs at most on both platforms, and the tab-bar wiring in a skeleton we test on both simulators before it ships.