Summary
On Android with edge-to-edge enabled (RN edgeToEdgeEnabled=true, transparent navigation bar), ModalBottomSheet / BottomSheet content is laid out flush to the physical bottom of the window. Primary actions (Cancel / Done) sit under the gesture nav / 3-button bar.
The host already accounts for the top system inset (statusBars + displayCutout → contentRegionInset / Yoga padding). There is no equivalent for navigationBars() (or iOS home-indicator) at the bottom.
JS workarounds (paddingBottom / marginBottom on footers from useSafeAreaInsets()) are unreliable under the library’s Yoga + native translation model. What works for apps today is a flex sibling spacer with a hardcoded floor (e.g. 64dp) — a brittle consumer-side hack.
Environment
@swmansion/react-native-bottom-sheet: ^0.16.2
- React Native:
0.86 (New Architecture / Fabric)
edgeToEdgeEnabled=true in gradle.properties
- Transparent
navigationBarColor / translucent system bars
- Reproduced on physical Android (gesture and 3-button nav)
iOS home indicator has the same class of problem for footers pinned to the sheet bottom.
Reproduction
import { useState } from "react";
import { Button, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ModalBottomSheet } from "@swmansion/react-native-bottom-sheet";
export function Example() {
const [index, setIndex] = useState(0);
const insets = useSafeAreaInsets();
return (
<>
<Button title="Open" onPress={() => setIndex(1)} />
<ModalBottomSheet
detents={[0, "content"]}
index={index}
onIndexChange={setIndex}
scrimColor="rgba(0,0,0,0.35)"
surface={<View style={{ flex: 1, backgroundColor: "white" }} />}
>
<View style={{ padding: 16, gap: 12 }}>
<Text>Actions at the bottom of the sheet</Text>
{/* Even with insets.bottom, Cancel often still sits under the nav bar */}
<View style={{ paddingBottom: insets.bottom }}>
<Button title="Cancel" onPress={() => setIndex(0)} />
</View>
</View>
</ModalBottomSheet>
</>
);
}
Expected: Cancel clears the system navigation / home indicator; sheet surface may still extend edge-to-edge underneath.
Actual: Cancel is obscured by the system nav. Footer padding/margin is often ineffective; apps need a consumer-side flex sibling gap + platform floor.
Why (from reading the native host)
In BottomSheetHostView.recomputeNativeGeometry() only the top inset is read:
val topInset =
ViewCompat.getRootWindowInsets(this)
?.getInsets(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.displayCutout())
?.top ?: 0
That drives contentRegionInset / detent cap. The sheet container still anchors to the physical bottom of an edge-to-edge window. Absolutely positioned surface fills that region (correct for chrome), but in-flow content has no native bottom inset for navigationBars().
The README example of paddingBottom: insets.bottom assumes Yoga padding on children is enough; under Fabric + this host’s translation / content-offset path, that is not consistently true for footers.
Proposed API (either)
A) Automatic (preferred for edge-to-edge defaults)
When the window is edge-to-edge, apply navigationBars() (and iOS safe-area bottom) as a content-bottom inset, analogous to today’s top contentRegionInset:
- Surface still edge-to-edge (full sheet chrome under the nav bar).
- In-flow content / scrollables lay out above the nav bar.
B) Explicit prop
<ModalBottomSheet bottomInset={insets.bottom} ...>
or edges={['bottom']} / safeAreaBottom — applied natively in the content region (not only documented as a JS padding tip).
Related: #62 (native footer slot) and any detached-sheet bottomInset discussion — a shared native bottom inset would help both.
Sketch: Android inset change
Illustrative only — naming/state plumbing should match existing contentRegionInset / pushGeometryState patterns.
// BottomSheetHostView.recomputeNativeGeometry()
val rootInsets = ViewCompat.getRootWindowInsets(this)
val topInset =
rootInsets
?.getInsets(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.displayCutout())
?.top ?: 0
val bottomInset =
rootInsets
?.getInsets(WindowInsetsCompat.Type.navigationBars())
?.bottom ?: 0
val location = IntArray(2)
getLocationInWindow(location)
val topOverlap = (topInset - location[1]).coerceAtLeast(0)
val rootViewHeight = (rootView?.height ?: height)
val bottomOverlap =
(location[1] + height - (rootViewHeight - bottomInset)).coerceAtLeast(0)
val cap =
(if (extendUnderStatusBar) height else height - topOverlap)
.toFloat()
.coerceAtLeast(0f)
// Push both into state, e.g.:
// contentRegionInsetTop → existing Yoga bottom-padding trick for top gap
// contentRegionInsetBottom → NEW: so in-flow content clears nav
pushGeometryState(
width = width,
height = height,
insetTop = height - cap,
insetBottom = bottomOverlap.toFloat(),
)
In the Fabric component descriptor / content wrapper, reserve insetBottom for in-flow children (Yoga padding or an extra content inset), while keeping surface on absoluteFill so the sheet chrome still draws under the system bar.
iOS: mirror with safeAreaInsets.bottom on BottomSheetHostingView when computing the content region.
Consumer workaround (today)
Until this lands, reserve a flex sibling under sheet children with a stable platform floor (avoid post-settle measureInWindow — it overshoots and causes layout jump):
<View style={{ flex: 1 }}>
<View style={{ flex: 1, minHeight: 0 }}>{children}</View>
<View style={{ height: Math.max(insets.bottom, Platform.OS === "android" ? 64 : 34) }} />
</View>
Footer paddingBottom: insets.bottom did not reliably clear the nav; a flex sibling spacer did.”
Happy to test a PR on RN 0.86 + edge-to-edge if useful.
Summary
On Android with edge-to-edge enabled (RN
edgeToEdgeEnabled=true, transparent navigation bar),ModalBottomSheet/BottomSheetcontent is laid out flush to the physical bottom of the window. Primary actions (Cancel / Done) sit under the gesture nav / 3-button bar.The host already accounts for the top system inset (
statusBars+displayCutout→contentRegionInset/ Yoga padding). There is no equivalent fornavigationBars()(or iOS home-indicator) at the bottom.JS workarounds (
paddingBottom/marginBottomon footers fromuseSafeAreaInsets()) are unreliable under the library’s Yoga + native translation model. What works for apps today is a flex sibling spacer with a hardcoded floor (e.g. 64dp) — a brittle consumer-side hack.Environment
@swmansion/react-native-bottom-sheet:^0.16.20.86(New Architecture / Fabric)edgeToEdgeEnabled=trueingradle.propertiesnavigationBarColor/ translucent system barsiOS home indicator has the same class of problem for footers pinned to the sheet bottom.
Reproduction
Expected: Cancel clears the system navigation / home indicator; sheet surface may still extend edge-to-edge underneath.
Actual: Cancel is obscured by the system nav. Footer padding/margin is often ineffective; apps need a consumer-side flex sibling gap + platform floor.
Why (from reading the native host)
In
BottomSheetHostView.recomputeNativeGeometry()only the top inset is read:That drives
contentRegionInset/ detent cap. The sheet container still anchors to the physical bottom of an edge-to-edge window. Absolutely positionedsurfacefills that region (correct for chrome), but in-flow content has no native bottom inset fornavigationBars().The README example of
paddingBottom: insets.bottomassumes Yoga padding on children is enough; under Fabric + this host’s translation / content-offset path, that is not consistently true for footers.Proposed API (either)
A) Automatic (preferred for edge-to-edge defaults)
When the window is edge-to-edge, apply
navigationBars()(and iOS safe-area bottom) as a content-bottom inset, analogous to today’s topcontentRegionInset:B) Explicit prop
or
edges={['bottom']}/safeAreaBottom— applied natively in the content region (not only documented as a JS padding tip).Related: #62 (native footer slot) and any detached-sheet
bottomInsetdiscussion — a shared native bottom inset would help both.Sketch: Android inset change
Illustrative only — naming/state plumbing should match existing
contentRegionInset/pushGeometryStatepatterns.In the Fabric component descriptor / content wrapper, reserve
insetBottomfor in-flow children (Yoga padding or an extra content inset), while keepingsurfaceonabsoluteFillso the sheet chrome still draws under the system bar.iOS: mirror with
safeAreaInsets.bottomonBottomSheetHostingViewwhen computing the content region.Consumer workaround (today)
Until this lands, reserve a flex sibling under sheet children with a stable platform floor (avoid post-settle
measureInWindow— it overshoots and causes layout jump):Footer paddingBottom: insets.bottom did not reliably clear the nav; a flex sibling spacer did.”
Happy to test a PR on RN 0.86 + edge-to-edge if useful.