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

Open
#4,664 3 comments 0 reactions 1 assignee View on GitHub

@kacperzolkiewski is already working on this.

Since Sep 17, 2026.

cr:missing-info cr:missing-repro cr:platform:android missing-info missing-repro platform:android
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-screens is 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.ktcontentView is added with LayoutParams(MATCH_PARENT, MATCH_PARENT) and bottomNavigationView with (MATCH_PARENT, WRAP_CONTENT, Gravity.BOTTOM), added after it, so the bar overlays full-height content.
  • TabsContainer.ktoverride 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, calls setOnInterfaceInsetsChangeListener, reads getInterfaceInsets(), and has an onInterfaceInsetsChange path.
  • NativeTabsView.android.js — wraps each screen in SafeAreaView collapsable={false} style={{flex:1}} edges={{bottom:true}} unless disableAutomaticContentInsets is 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

  1. An Expo SDK 57 app with expo-router's NativeTabs, on a physical Android device.
  2. In any tab screen, render a child with position: "absolute", left: 0, right: 0, bottom: 16.
  3. Observe that it is drawn behind the tab bar and is not visible.
  4. Log measureInWindow on the screen root: its y + height equals the full window height, showing no bottom inset was applied.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.