Pre-alphaThe registry and the CLI are not published yet.Roadmap

BottomSheet

Compound bottom sheet with snap points, gestures and scrollable content.

LT

Linus TorvaldsHelsinki
Share

A

Ada

B

Alan

G

Grace

M

Margaret
Copy link
Save to Files

Installation

npx axiom add bottom-sheet
pnpm dlx axiom add bottom-sheet
yarn dlx axiom add bottom-sheet
bun x axiom add bottom-sheet

Also copies: portal, overlay, tappable, slot, title, icon-button.

Requires the close icon in your icon registry.

The sheet follows the keyboard with react-native-keyboard-controller. It’s a native module: rebuild your development build after installing it, and wrap your app in its KeyboardProvider:

app/_layout.tsx
import { KeyboardProvider } from "react-native-keyboard-controller";

<GestureHandlerRootView style={{ flex: 1 }}>
	<KeyboardProvider>
		<PortalProvider>{/* … */}</PortalProvider>
	</KeyboardProvider>
</GestureHandlerRootView>;

Installs: react-native-reanimated, react-native-worklets, react-native-gesture-handler, react-native-safe-area-context.

The sheet renders in the root PortalHost, and its gestures need GestureHandlerRootView above it. Wrap your root layout once:

app/_layout.tsx
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { PortalHost, PortalProvider } from "@/components/core/portal";

export default function RootLayout() {
	return (
		<GestureHandlerRootView style={{ flex: 1 }}>
			<PortalProvider>
				<Stack />
				<PortalHost />
			</PortalProvider>
		</GestureHandlerRootView>
	);
}

Usage

import { BottomSheet } from "@/components/ui/bottom-sheet";

<BottomSheet.Root>
	<BottomSheet.Trigger asChild>
		<Button>Share</Button>
	</BottomSheet.Trigger>
	<BottomSheet.Content snapPoints={["50%", "90%"]}>
		<BottomSheet.Handle />
		<BottomSheet.Header title="Share" />
		{/* content */}
	</BottomSheet.Content>
</BottomSheet.Root>;

Props

BottomSheet.Root

PropTypeDefaultDescription
openboolean—Controlled visibility.
defaultOpenbooleanfalseInitial visibility when the sheet manages its own state.
onOpenChange(open: boolean) => void—Called when the sheet opens or closes, including by swipe, backdrop press or a back action (Android back button, back gesture with a navigator).
childrenReactNode—Trigger and Content.

BottomSheet.Trigger

PropTypeDefaultDescription
asChildbooleanfalseOpens the sheet from the child element instead of rendering a pressable.
childrenReactNode—The element that opens the sheet.

BottomSheet.Content

PropTypeDefaultDescription
snapPointsArray<number | string>['content']Heights the sheet rests at, from lowest to highest: points (320), a percentage of the screen ('50%'), or 'content' to size it to its children.
indexnumber0Controlled snap point.
onIndexChange(index: number) => void—Called when the sheet settles on another snap point.
overlaybooleantrueDims the screen behind the sheet through Overlay. The dimming follows the sheet position.
dismissiblebooleantrueCloses on swipe down, backdrop press and back actions. false for a step the user must complete.
detachedbooleanfalseFloats above the bottom edge with all corners rounded, for short confirmations.
stackbooleantrueScales the sheet underneath when this one opens over it, and restores it on close. See Stacking.
hapticHapticKind | false—Haptic played when a drag lands the sheet on another snap point. Off unless you pass a kind.
keyboardBehavior'interactive' | 'extend' | 'none''interactive'How the sheet reacts when an input inside it opens the keyboard. interactive rises with the keyboard frame by frame, without going past the top safe area. extend also moves to the highest snap point. none leaves the sheet in place. Closing the sheet dismisses the keyboard.
footerReactNode—Floating actions pinned above the bottom safe area, visible at every snap point.
onDismiss() => void—Called after the close animation ends.
styleStyleProp<ViewStyle>—Extra styles for the sheet surface.

BottomSheet.Handle, BottomSheet.Header, BottomSheet.ScrollView

PartPropsDescription
HandlestyleThe grab bar. Drag it to resize. Announced as “Resize sheet” with up and down actions.
Headertitle, leading, trailing, closeButton, styleTitle row. closeButton adds an IconButton that closes the sheet, with the close icon of your registry.
ScrollView / FlatListsame as React NativeScrollable content that hands the gesture back to the sheet when scrolled to the top.

Haptics

Pass a kind to BottomSheet.Content, usually "light", to play a haptic when the user drags the sheet onto another snap point. Opening, closing and index changes made in code play nothing: the haptic answers the finger. Nothing plays until your app connects a haptics library: see Haptics.

Use cases

Actions on an item

snapPoints={['content']}: the sheet takes the height it needs and no more.

IMG_0142.HEIC3.1 MB · Today

Share

Duplicate

Delete

<BottomSheet.Content detached>
	<BottomSheet.Handle />
	<FileSummary file={file} />
	<OptionItem label="Share" trailing={<Icon name="share" />} onPress={share} />
	<OptionItem label="Delete" destructive onPress={confirmDelete} />
</BottomSheet.Content>

Short form without leaving the screen

The sheet rises above the keyboard; the main button lives in footer so it stays reachable.

CancelNew listCreate
Trip to Porto
qwertyuiop
<BottomSheet.Content snapPoints={["content"]} keyboardBehavior="extend">
	<BottomSheet.Header
		leading={
			<Button variant="ghost" onPress={close}>
				Cancel
			</Button>
		}
		title="New list"
		trailing={
			<Button variant="ghost" disabled={!name} onPress={create}>
				Create
			</Button>
		}
	/>
	<Input autoFocus value={name} onChangeText={setName} accessibilityLabel="List name" />
</BottomSheet.Content>

Map with a draggable list

Several snap points over content that stays interactive. The list scrolls once the sheet is fully open.

Café Lomi350 m · Open until 18:00
Jardin des Plantes1.2 km
<BottomSheet.Root open>
	<BottomSheet.Content
		snapPoints={[120, "45%", "90%"]}
		overlay={false}
		dismissible={false}
	>
		<BottomSheet.Handle />
		<SearchBar />
		<BottomSheet.FlatList data={places} renderItem={renderPlace} />
	</BottomSheet.Content>
</BottomSheet.Root>

Stacking

Open a sheet from inside another one and the first doesn’t just sit there under the second. It scales down and stays behind, dimmed by the new sheet’s overlay, while the new one rises in front of it.

Filters
Sort byPrice
Availability
Sort by
Price
Distance
Rating

There is nothing to install and nothing to turn on. Write two sheets and open the second one:

<BottomSheet.Root open={filtersOpen} onOpenChange={setFiltersOpen}>
	<BottomSheet.Content snapPoints={["60%"]}>
		<BottomSheet.Handle />
		<BottomSheet.Header title="Filters" />
		<OptionItem label="Sort by" value={sort} onPress={() => setSortOpen(true)} />
	</BottomSheet.Content>
</BottomSheet.Root>

<BottomSheet.Root open={sortOpen} onOpenChange={setSortOpen}>
	<BottomSheet.Content>
		<BottomSheet.Handle />
		<BottomSheet.Header title="Sort by" closeButton />
		<SortOptions value={sort} onChange={setSort} />
	</BottomSheet.Content>
</BottomSheet.Root>

Both roots sit at screen level, side by side. Nesting the second root inside the first one’s content works too, and ties their lifetimes together: closing the filters sheet unmounts the sort sheet with it.

With a single sheet open, none of this runs and nothing about the sheet changes.

The movement

Opening and closing are not mirror images of each other.

alone

One sheet. Nothing about it differs from a sheet with no stack behind it.

opening

Both springs start together: the one underneath shrinks, the new one rises.

stacked

Settled. The sheet underneath is narrower, dimmed by the new overlay, and inert.

closing

The top sheet slides out without scaling, while the one underneath grows back.

The sheet underneath only ever scales. It doesn’t move up, it doesn’t move down; it shrinks by metrics.stackScale and comes back to full size later, from where it was.

The sheet on top only ever slides. It rises into place when it opens and drops off the bottom when it closes, at full size the whole way. Nothing about it says it is part of a stack.

Both animations start on the same frame and use the same spring, so the sheet underneath is already growing back while the one above is still on its way out.

A third sheet scales the second and leaves the first where it is: depth stops counting at two, so a sheet never shrinks past stackScale².

What the top sheet takes over

Being covered isn’t only a matter of scale. A covered sheet hands over everything that can be aimed at it:

ConcernBehavior
DragOnly the sheet on top responds, so a buried sheet can’t be resized or swiped away by accident.
TouchesA covered sheet stops taking presses. A button half-visible behind the new sheet can’t be hit.
Back actionThe Android back button and the back gesture close the sheet on top.
BackdropPressing it closes the sheet on top, not the pile.
Screen readerThe sheet on top is the only modal. Covered sheets leave the accessibility tree.

Overlays stay per sheet. The new sheet brings its own, which is what dims the one underneath and gives the recession its depth — the price is that the app behind two sheets is dimmed twice. overlay={false} on the second sheet removes that second layer, along with the dimming of the first sheet.

Leaving the stack

A sheet with stack={false} opens over the current one without scaling it. Use it when the second sheet interrupts rather than continues — a confirmation over the sheet that asked for it:

<BottomSheet.Content stack={false} detached snapPoints={["content"]}>
	<Text>Delete this list?</Text>
</BottomSheet.Content>

It still takes the drag, the backdrop and the back action while it’s on top. What it doesn’t do is touch the sheet underneath.

Dialog and Menu

A Dialog and a Menu join the same stack, with nothing to set up.

A dialog opened from a sheet pushes it back one level, like a second sheet would. A menu takes the top spot but leaves the sheet at full size: it is anchored to a trigger on that sheet, and scaling the sheet would slide the trigger out from under it. Either way, the sheet underneath hands over the drag, the touches, the back action and the screen reader until the dialog or menu closes.

When to use it

Use a sheet when the user needs to act without losing the current screen: pick an option, fill a short form, confirm details. See Mobile design guidelines.