Components
BottomSheet
The mobile pattern for content layered over a map, a player, or a feed: a sheet that rests at snap points, drags between them, and dismisses when flung down. Pure CSS transforms — no motion adapter needed, 60fps drags.
Over a map (non-modal)
Pass modal={false} and the page behind stays fully interactive — no backdrop, no scroll lock. Snap points are viewport fractions: the sheet opens at defaultSnap, dragging the grabber moves it between points, and dragging well below the lowest one calls onClose.
import { AppShell, BottomSheet } from "appshell-react";
import { useState } from "react";
export default function App() {
const [open, setOpen] = useState(true);
return (
<AppShell safeArea>
<MapCanvas />
<BottomSheet
open={open}
onClose={() => setOpen(false)}
modal={false} // the map behind stays interactive
snapPoints={[0.4, 0.85]} // rest at 40%, drag up to 85%
onSnapChange={(i) => console.log("snapped to", i)}
aria-label="Nearby places"
>
<PlacesList /> {/* scrolls internally */}
</BottomSheet>
</AppShell>
);
}As a modal
Left modal (the default), the sheet behaves like a dialog: a dimmed backdrop that closes on tap, body scroll locking, Escape to close, and a focus trap that hands focus back where it came from.
// Modal by default: backdrop, scroll lock, Escape, focus trap.
<BottomSheet open={open} onClose={close} snapPoints={[0.5]}>
<ShareOptions />
</BottomSheet>env(safe-area-inset-bottom), and the panel centers at max-w-lg on larger screens so a phone-wide sheet never stretches across a desktop.Overlay stack
A modal BottomSheet joins the same overlay stack as the Sidebar drawer, SearchModal, and the UserMenu/NotificationsMenu dropdowns — one shared mechanism behind every overlay in the library, so they nest correctly instead of interfering with each other:
- • Close requests. One listener for the whole stack answers
Escape(and, where supported, the platform’s back gesture) by closing only the topmost dismissable layer — open a menu inside this sheet, and one press closes the menu, not both. - • Scroll lock. A single ref-counted owner locks body scroll while any modal layer is open, and releases it only once the last one closes, so layering a second modal over the sheet never leaves the page stuck scrollable or stuck locked.
- • Stacking. z-index follows open order rather than component identity, so whatever you open from inside the sheet always lands above it.
API
| Prop | Type | Default | Description |
|---|---|---|---|
| open* | boolean | — | Whether the sheet is visible. |
| onClose* | () => void | — | Called on Escape / backdrop tap (modal) and when dragged below the lowest snap. |
| snapPoints | number[] | [0.45, 0.9] | Resting heights as viewport fractions, ascending. Dragging moves between them. |
| defaultSnap | number | 0 | Index into snapPoints the sheet opens at. |
| onSnapChange | (index: number) => void | — | Fires when a drag settles on a different snap point. |
| modal | boolean | true | Modal: backdrop, scroll lock, Escape, focus trap. false keeps the page behind interactive — the map pattern. |
| className | string | — | Extra classes for the panel. |
| aria-label | string | "Sheet" | Accessible dialog name. |
| children* | ReactNode | — | Sheet content — scrolls internally below the grabber. |