software-mansion / software-mansion/react-native-screens
[Android][NativeTabs] SafeAreaView reports a zero interface inset, so screen content is not inset by the tab bar
@kacperzolkiewski is already working on this.
Since Sep 17, 2026.
- Dominant language
- TypeScript
- Stars
- 3.7k
- Forks
- 714
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 71
Description
Description
On Android, a screen inside <NativeTabs> is laid out at the full window height with the BottomNavigationView drawn over it, and the SafeAreaView edges={{bottom: true}} that NativeTabsView.android.js wraps each screen in reports a zero interface inset. The result is that content is not inset by the tab bar at all: scrolling content passes underneath it, and any absolutely-positioned chrome anchored to bottom is rendered completely behind the bar.
Version honesty up front: observed on 4.26.2. I reviewed the 4.27.0 and 4.28.0 release notes and found no entry addressing interface insets or the tabs
SafeAreaView, so I believe it is still present — but I have not verified on 4.28.0.react-native-screensis a native module and this project's builds go through EAS cloud, so re-testing is not a local step. Happy to do it if that's the first thing you'd want.
What I measured
Galaxy S23, Expo SDK 57, expo-router 57.0.17, react-native-screens 4.26.2, New Architecture (non-disableable on SDK 55+). Logged from the device via onLayout + measureInWindow:
window 402×874, screen 402×874
screen root inWindow y=104 h=770 bottomEdge=874 ← full window height
overlay inWindow y=810 h=48 bottomEdge=858 ← entirely behind the bar
insets: initialWindowMetrics {top:62, right:0, bottom:34, left:0}
useSafeAreaInsets() {top:62, right:0, bottom:34, left:0} ← inside the tab screen
The screen root's bottom edge is exactly the window height (104 + 770 = 874), so no bottom inset is applied. Both safe-area readings report only the 34dp gesture inset — never the ~72–80dp the bar occupies.
This is not caused by app-side composition. I re-measured with <NativeTabs> as the bare route root — no wrapper View, no custom header, no StatusBar — and got y=0, h=874, bottomEdge=874. Identical: still no inset.
Source trace
TabsContainer.kt—contentViewis added withLayoutParams(MATCH_PARENT, MATCH_PARENT)andbottomNavigationViewwith(MATCH_PARENT, WRAP_CONTENT, Gravity.BOTTOM), added after it, so the bar overlays full-height content.TabsContainer.kt—override fun getInterfaceInsets(): EdgeInsets = EdgeInsets(0f, 0f, 0f, bottomNavigationView.height.toFloat()), so the container does intend to report the bar's height.safearea/SafeAreaProvider.kt— "This protocol only handles interface insets (e.g. toolbar, bottomNavigationView)."safearea/SafeAreaView.kt— finds a provider, callssetOnInterfaceInsetsChangeListener, readsgetInterfaceInsets(), and has anonInterfaceInsetsChangepath.NativeTabsView.android.js— wraps each screen inSafeAreaView collapsable={false} style={{flex:1}} edges={{bottom:true}}unlessdisableAutomaticContentInsetsis set (it was not).
So the machinery is present and ungated, and the value arriving at the JS side is nevertheless zero. My best guess is a timing issue — getInterfaceInsets() read while bottomNavigationView.height is still 0, with the change listener not subsequently firing — but I have not instrumented the native side to confirm that, so I'd rather not assert it.
Why it is worth fixing rather than working around
There is no app-side workaround available, because the bar's height is not exposed to JS anywhere. Both safe-area sources report only the gesture inset, and NativeTabsProps / TabsHostProps / TabsScreenProps expose no height, callback or event for it (I enumerated every prop across both packages). iOS's counterpart for chrome above the bar, BottomAccessory, is iOS 26+ only and has no Android equivalent.
The practical consequence for us was that a shipped search bar — an overlay at bottom: 16 — became invisible after adopting NativeTabs, with no failing test, because the JS test environment performs no layout.
Possibly related
#4132 ([Android][NativeTabs] Pressables in bottom ~56dp ... don't fire when hidden=true) describes a different symptom under a different condition (hidden={true}, touches consumed rather than content occluded), so I did not file this as a duplicate. But its observation that "the dead zone is anchored to the screen bottom in window coordinates (it tracks the BottomNavigationView's measured strip)" independently matches what I measured here: content occupies the full window height and the bar's strip sits over it. They may share a root cause.
Steps to reproduce
- An Expo SDK 57 app with
expo-router'sNativeTabs, on a physical Android device. - In any tab screen, render a child with
position: "absolute", left: 0, right: 0, bottom: 16. - Observe that it is drawn behind the tab bar and is not visible.
- Log
measureInWindowon the screen root: itsy + heightequals the full window height, showing no bottom inset was applied.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.