sheetDemo.mov
@0xbridges/sheet is a React Native library for performant bottom sheets and top sheets where:
- snap points accept pixels, percentages,
"content"sizing, and measured anchors - detached sheets can morph into fullscreen with live corner-radius interpolation
- dismissal rubber-bands at the floor instead of freezing when
dismissible={false} collapsedHeightadds a persistent peek state for maps-style attached sheets- scrollable content hands off between sheet drag and inner scroll automatically
- the backdrop can be configured to pass through touches or close on tap
The library exports two main components: BottomSheet (slides up from the bottom) and TopSheet (slides down from the top). Both share the same snap-point system, gesture engine, and animation behavior.
If you want a shorter machine-oriented summary, see llms.txt.
BottomSheet is a flexible sheet container that slides up from the bottom of the screen. TopSheet is its counterpart that slides down from the top.
Both components handle:
- spring-based snap animation
- multi-stop snap points
- horizontal gesture rejection
- backdrop opacity interpolation
- rubber-band overscroll at edges
- detached floating card mode
- fullscreen expansion with corner morph
- scroll-to-drag handoff for inner scrollable content
- anchor-based snap points measured from child layout
These are the important behavior rules (they apply to both BottomSheet and TopSheet unless noted):
- The sheet snaps to the nearest snap point on release, projected by velocity.
- Swiping past the floor dismisses when
dismissibleis true. - When
dismissible={false}, swiping below the floor rubber-bands instead of dismissing. ForTopSheetat fullscreen withdismissible={false}, dragging collapses back to the lowest snap point instead of dismissing. - Backdrop opacity scales between floor and ceiling heights. Below the floor it fades toward zero.
- The backdrop blocks touches when
backdropPressBehavior="close"and passes them through with"none". - Anchor snap points are measured from child layout using
BottomSheetAnchor/TopSheetAnchormarker views. BottomSheetScrollView/TopSheetScrollViewenables scroll-to-drag handoff: the sheet drags until it reaches the ceiling, then inner scrolling takes over.- Detached sheets float with configurable padding and can morph into fullscreen when
allowFullScreenis set.
- The drag handle starts at the bottom when collapsed and transitions to the top as the sheet expands to fullscreen.
- At fullscreen the handle includes safe-area padding so content starts below the Dynamic Island / notch.
- Fullscreen dismissal uses a slide-down gesture (like swiping a notification away). When
dismissible={false}, this gesture collapses back to the lowest snap instead of dismissing.
Install the package and make sure your app already has the required gesture/animation setup.
npm install @0xbridges/sheet react-native-gesture-handler react-native-reanimated react-native-safe-area-contextYour app must have:
react-native-gesture-handlerreact-native-reanimatedreact-native-safe-area-context- the normal configuration required by those libraries for your React Native or Expo setup
import { BottomSheet } from "@0xbridges/sheet";
export function Example() {
return (
<BottomSheet
snapPoints={["content", "72%"]}
allowFullScreen
initialSnapIndex={0}
>
<YourContent />
</BottomSheet>
);
}import { TopSheet } from "@0xbridges/sheet";
export function Example() {
return (
<TopSheet
snapPoints={["42%"]}
allowFullScreen
detached
detachedPadding={{ horizontal: 12, top: 12 }}
cornerRadius={56}
fullScreenCornerRadius={0}
initialSnapIndex={0}
>
<YourContent />
</TopSheet>
);
}Snap points define the heights the sheet can rest at. Pass them as snapPoints.
A number is treated as an absolute height in points.
snapPoints={[220, 440]}A string like "56%" is a percentage of the available height (viewport minus top inset).
snapPoints={["32%", "56%", "84%"]}The string "content" measures the sheet content's natural height and uses it as a snap point.
snapPoints={["content", "72%"]}The natural content height is measured once per presentation in an off-screen container, then the measurement tree unmounts so children render exactly once. If your content can change size while the sheet is open, prefer pixel, percentage, or anchor snap points instead.
Anchor snap points are measured from BottomSheetAnchor marker views placed inside the sheet content. Each anchor produces a snap height equal to the anchor's bottom edge plus an optional offset.
import { createBottomSheetAnchor, BottomSheetAnchor } from "@0xbridges/sheet";
const summaryAnchor = createBottomSheetAnchor("summary", { offset: 18 });
const detailAnchor = createBottomSheetAnchor("detail", { offset: 18 });
<BottomSheet snapPoints={[summaryAnchor, detailAnchor]} allowFullScreen>
<BottomSheetAnchor name="summary">
<SummarySection />
</BottomSheetAnchor>
<BottomSheetAnchor name="detail">
<DetailSection />
</BottomSheetAnchor>
</BottomSheet>The name on BottomSheetAnchor must match the key passed to createBottomSheetAnchor.
When allowFullScreen is true, the sheet adds the full available height as an extra snap point above all configured stops.
Array of snap point definitions. Accepts pixels, percentages, "content", and anchor points. Default: ["content"].
Default: 0
Which snap point to open at when the sheet first presents.
Controlled open state. When provided, the sheet is controlled and you must update this value in response to onOpenChange.
Default: true
Initial open state for uncontrolled usage.
Default: false
Adds the full available height as an additional snap point above the highest configured stop.
Default: true
When true, swiping below the floor or tapping the backdrop dismisses the sheet. When false, the sheet rubber-bands at its lowest snap point.
A snap point definition that becomes the sheet's persistent floor. The sheet cannot be dismissed below this height. Useful for maps-style peek states.
Default: false
Floats the sheet above the bottom edge with padding. Renders rounded corners on all four sides.
Padding around a detached sheet. Accepts a number (uniform) or an object with bottom, horizontal, left, right, top, and vertical fields.
Default: 28
Border radius for the sheet's top corners (all four corners when detached).
Corner radius when the sheet is at fullscreen height. Defaults to cornerRadius for attached sheets and 0 for detached sheets. The radius interpolates live during drag.
Default: "sheet"
"sheet": the entire sheet surface is draggable."handle": only the handle area responds to drag. Use withBottomSheetScrollViewfor scroll-to-drag handoff.
Default: 0.34
Maximum backdrop opacity at the highest snap point. Set to 0 to hide the backdrop.
Default: "close"
"close": tapping the backdrop dismisses the sheet."none": the backdrop is visible but passes touches through to the content behind it.
Default: "#000000"
Additional style applied to the backdrop pressable.
Default: true
Shows or hides the drag handle indicator.
Default: "rgba(255, 255, 255, 0.42)"
Additional style applied to the handle area.
Style applied to the sheet container. Use for background color.
Style applied to the content wrapper inside the sheet.
Default: true
When true, adds bottom safe-area padding to the content container.
Default: 0
Extra bottom inset added to the content container, on top of the safe-area inset.
Default: 0
Top inset that limits the sheet's maximum height. When allowFullScreen is true, the larger of topInset and the safe-area top is used.
Style applied to the root overlay container.
(open: boolean) => void
Called when the sheet's open state changes. Required for controlled usage.
() => void
Called when the sheet finishes dismissing.
(index: number, height: number) => void
Called when the sheet settles at a snap point. Receives the snap index and the resolved height in points.
Access imperative methods via a ref.
const sheetRef = useRef<BottomSheetRef>(null);
<BottomSheet ref={sheetRef} ...>
sheetRef.current?.present();
sheetRef.current?.dismiss();
sheetRef.current?.expand();
sheetRef.current?.snapToIndex(1);Opens the sheet at initialSnapIndex.
Closes the sheet with a spring animation.
Snaps to the highest configured snap point.
Snaps to the snap point at the given index.
Use BottomSheetScrollView for scrollable content inside the sheet.
When dragRegion="sheet", the scroll view coordinates with the sheet gesture:
- While the sheet is below its ceiling, dragging moves the sheet.
- Once the sheet reaches its ceiling, inner scrolling activates.
- Pulling down from scroll offset zero hands the gesture back to the sheet.
import { BottomSheet, BottomSheetScrollView, useBottomSheetInsets } from "@0xbridges/sheet";
function SheetContent() {
const insets = useBottomSheetInsets();
return (
<BottomSheetScrollView
contentContainerStyle={{ paddingBottom: insets.bottom + 20 }}
>
<LongContent />
</BottomSheetScrollView>
);
}
<BottomSheet
snapPoints={["48%", "82%"]}
allowFullScreen
dragRegion="sheet"
applyContentInset={false}
>
<SheetContent />
</BottomSheet>Returns { bottom: number } representing the content bottom inset (safe area + contentBottomInset). Use this to add padding inside BottomSheetScrollView.
BottomSheetScrollView / TopSheetScrollView accept an optional onScroll callback typed as (event: NativeScrollEvent) => void — the raw native event, not a NativeSyntheticEvent wrapper. Read offsets directly off the event:
<BottomSheetScrollView
onScroll={(event) => {
console.log(event.contentOffset.y);
}}
/>Pass a Reanimated worklet to handle scroll on the UI thread with zero bridge crossings — recommended for parallax, sticky headers, and any per-frame work driven by scroll position:
import { useAnimatedScrollHandler, useSharedValue } from "react-native-reanimated";
const scrollY = useSharedValue(0);
const handleScroll = useAnimatedScrollHandler((event) => {
"worklet";
scrollY.value = event.contentOffset.y;
});
<BottomSheetScrollView onScroll={handleScroll}>...</BottomSheetScrollView>;A regular JS function still works; it will be invoked via runOnJS with one bridge crossing per scroll event.
Detached sheets float above the bottom edge, rounded on all four corners.
<BottomSheet
detached
detachedPadding={{ bottom: 16, horizontal: 16 }}
snapPoints={["46%"]}
>
<CardContent />
</BottomSheet>When combined with allowFullScreen, the sheet morphs from a floating card to fullscreen. The corner radius, margins, and shadow interpolate smoothly during the transition.
<BottomSheet
dismissible={false}
snapPoints={[220, "56%"]}
>
<Content />
</BottomSheet>The sheet rubber-bands at its lowest snap point instead of dismissing. Backdrop tap is ignored.
<BottomSheet
collapsedHeight={136}
dismissible={false}
backdropOpacity={0}
backdropPressBehavior="none"
snapPoints={["48%", "84%"]}
>
<PeekContent />
</BottomSheet>collapsedHeight is prepended to the snap points as the floor. Combined with dismissible={false}, this creates a persistent peek that the user can swipe up to expand.
TopSheet is the top-of-screen counterpart to BottomSheet. It slides down from the top edge and supports the same snap-point system, detached mode, fullscreen morphing, and scroll handoff.
TopSheet accepts the same props as BottomSheet with these differences:
| Prop | Default | Notes |
|---|---|---|
bottomInset |
0 |
Limits the sheet's maximum downward extent (analogous to topInset on BottomSheet). |
contentTopInset |
0 |
Extra top inset added to the content container, on top of the safe-area inset. |
applyContentInset |
true |
When true, adds top safe-area padding to the content container (BottomSheet adds bottom). |
All other props (snapPoints, allowFullScreen, detached, detachedPadding, cornerRadius, fullScreenCornerRadius, dismissible, dragRegion, backdropOpacity, backdropPressBehavior, handleVisible, handleColor, sheetStyle, contentContainerStyle, style, onOpenChange, onDismiss, onSnapChange, etc.) work identically.
Same as BottomSheet: present(), dismiss(), expand(), snapToIndex(index).
const sheetRef = useRef<TopSheetRef>(null);
<TopSheet ref={sheetRef} ...>
sheetRef.current?.present();
sheetRef.current?.dismiss();
sheetRef.current?.expand();
sheetRef.current?.snapToIndex(1);Use TopSheetScrollView for scrollable content inside a TopSheet, and useTopSheetInsets for safe-area padding.
import { TopSheet, TopSheetScrollView, useTopSheetInsets } from "@0xbridges/sheet";
function SheetContent() {
const insets = useTopSheetInsets();
return (
<TopSheetScrollView
contentContainerStyle={{ paddingTop: insets.top + 10 }}
>
<LongContent />
</TopSheetScrollView>
);
}
<TopSheet
snapPoints={["48%", "82%"]}
allowFullScreen
dragRegion="sheet"
applyContentInset={false}
>
<SheetContent />
</TopSheet>A detached TopSheet that morphs from a floating card to fullscreen:
<TopSheet
detached
detachedPadding={{ horizontal: 12, top: 12 }}
cornerRadius={56}
fullScreenCornerRadius={0}
allowFullScreen
snapPoints={["42%"]}
>
<CardContent />
</TopSheet>When collapsed, the sheet is a rounded floating card at the top. Dragging the handle down expands it to fullscreen. Corner radius, margins, and shadow interpolate smoothly. The drag handle transitions from the bottom of the card to the top when fullscreen.
A non-dismissible TopSheet can act as an embedded card that floats over scrollable page content:
import { TopSheet } from "@0xbridges/sheet";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useWindowDimensions } from "react-native";
export function PageWithEmbeddedSheet() {
const { height: screenHeight } = useWindowDimensions();
const safeArea = useSafeAreaInsets();
const sheetHeight = screenHeight * 0.42 + safeArea.top + 12;
return (
<View style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{ paddingTop: sheetHeight + 16 }}>
<PageContent />
</ScrollView>
<TopSheet
allowFullScreen
backdropOpacity={0}
backdropPressBehavior="none"
cornerRadius={56}
defaultOpen
detached
detachedPadding={{ horizontal: 12, top: safeArea.top + 12 }}
dismissible={false}
dragRegion="sheet"
fullScreenCornerRadius={0}
open
snapPoints={["42%"]}
>
<CardContent />
</TopSheet>
</View>
);
}The sheet floats at the top with no backdrop. Page content scrolls underneath it. Dragging the sheet expands it to fullscreen; from fullscreen it collapses back to the card (since dismissible={false} prevents full dismissal).
Works the same as BottomSheet anchors, using TopSheetAnchor and createTopSheetAnchor:
import { TopSheet, TopSheetAnchor, createTopSheetAnchor } from "@0xbridges/sheet";
const headerAnchor = createTopSheetAnchor("header", { offset: 18 });
<TopSheet snapPoints={[headerAnchor]} allowFullScreen>
<TopSheetAnchor name="header">
<HeaderSection />
</TopSheetAnchor>
<DetailSection />
</TopSheet>The package exports:
BottomSheet
BottomSheetBottomSheetAnchorBottomSheetScrollViewcreateBottomSheetAnchoruseBottomSheetInsetsBottomSheetAnchorPoint(type)BottomSheetAnchorProps(type)BottomSheetDetachedPadding(type)BottomSheetInsets(type)BottomSheetProps(type)BottomSheetRef(type)BottomSheetSnapPoint(type)BottomSheetScrollViewProps(type)
TopSheet
TopSheetTopSheetAnchorTopSheetScrollViewcreateTopSheetAnchoruseTopSheetInsetsTopSheetAnchorPoint(type)TopSheetAnchorProps(type)TopSheetDetachedPadding(type)TopSheetInsets(type)TopSheetProps(type)TopSheetRef(type)TopSheetSnapPoint(type)TopSheetScrollViewProps(type)
The library is designed around performance first.
It does the following:
- keeps gesture math on the UI thread with Reanimated worklets
- uses spring animations with velocity projection for natural snapping
- measures anchor positions in an off-screen container to avoid layout thrashing
- debounces anchor registration to batch rapid layout changes
- structurally caches
snapPointsandcollapsedHeightso inline arrays (e.g.snapPoints={["50%", "content"]}) don't cascade re-renders on every parent render - supports worklet
onScrollhandlers on the scroll view to skip the JS bridge during scroll
For best results:
- keep children stable across renders
- use
sheetStylefor background color instead of wrapping in extra views - prefer pixel or percentage snap points over anchors when the heights are known ahead of time
- set
applyContentInset={false}when usingBottomSheetScrollViewand handle padding manually viauseBottomSheetInsets
The repo includes a working Expo demo app in example/.
It covers bottom sheet scenarios (dynamic content, fixed height, percentage snaps, anchor snaps, detached cards, detached-to-fullscreen morphing, non-dismissible sheets, collapsible peek, multi-stop workflow snaps, scrollable fullscreen) and top sheet scenarios (detached, detached-to-fullscreen, scrollable fullscreen, and an embedded page example).
From this package directory:
npm install
npm run example:install
npm run example:start