Skip to main content

BottomSheet

A bottom-anchored modal panel native to mobile UX. Supports multiple snap points, drag-to-resize, swipe-to-dismiss, and backdrop tap dismiss. Use it for filter panels, detail pages, comment lists, action confirmations — anywhere a centered modal would feel out of place on touch.

Basic Usage

Snap points

snapPoints is an array of viewport-height fractions (0-1). The sheet maximum height is the largest entry; intermediate values are stopping points the user can drag to. Default is [0.5] (single fixed half-height).

<BottomSheet
open={open}
onClose={close}
snapPoints={[0.25, 0.5, 0.9]}
initialSnap={1}
>
<FilterPanel />
</BottomSheet>

This gives a 3-position sheet — peek (25%), default (50%), and fullscreen (90%). The user can drag the handle between any of them. Dragging below the smallest snap (or beyond 50% past it) calls onClose.

Picking snap points

PatternsnapPointsUse for
Single height[0.5]Action confirmations, simple forms
Peek + expand[0.25, 0.7]Map overlays, detail with progressive disclosure
Mini → half → full[0.2, 0.5, 0.92]Maps, music players, complex tools

Drag handle

By default a small handle is shown at the top of the sheet (--tui-bg-muted rounded bar). Set showHandle={false} to hide it — useful when you want a strict modal feel with no drag affordance.

<BottomSheet open={open} onClose={close} showHandle={false} dismissible={false}>
<RequiredConfirmation />
</BottomSheet>

Dismissibility

Set dismissible={false} to prevent swipe-down dismiss (e.g., for confirmation flows where the user must explicitly choose). The backdrop tap still closes the sheet unless you also handle the onClose callback.

PropEffect
dismissible={true} (default)Swipe down past threshold → onClose
dismissible={false}Swipe is captured but never dismisses; ESC + backdrop tap still work

Mobile considerations

  • The sheet uses .tui-glass surface (translucent + backdrop-blur on capable browsers, opaque --tui-bg-subtle fallback on older Android WebView).
  • Drag is GPU-accelerated (transform: translateY) — stays smooth even with heavy children.
  • Body scroll is locked while the sheet is open and restored on close.
  • Respects iOS safe-area-inset-bottom automatically (so content doesn't sit under the home indicator).
  • On prefers-reduced-motion: reduce, the slide-up animation is replaced with instant fade.
  • Pointer events use useDrag hook internally — both touch and mouse drag work.
  • Dialog with mobileVariant="sheet" — easier opt-in if you already use Dialog
  • ActionSheet — pre-built action list on top of BottomSheet
  • Menu with mobileVariant="sheet" — dropdown that converts to a sheet on touch

Props

PropTypeDefaultDescription
open*boolean-Whether the sheet is open (controlled)
onClose*() => void-Called when the sheet should close (backdrop tap, swipe-down, ESC, drag out-of-range)
snapPointsnumber[][0.5]Heights as fractions of viewport (0-1). Max value = sheet maximum height.
initialSnapnumber0Index into snapPoints when the sheet opens
showHandlebooleantrueShow the drag handle at the top
dismissiblebooleantrueAllow swipe-down to dismiss
childrenReactNode-Sheet content
classNamestring-Additional CSS class for the sheet element

Also accepts standard <div> HTML attributes via spread.