Skip to content
WANCRAFT

B.39Cards & Containers

Bottom Sheet

A draggable modal sheet that snaps between heights with velocity, built on a native dialog.

NewReactMotionTailwindadvanced

Overview

Bottom Sheet is a modal panel that rises from the bottom edge and rests on fractional snap points such as 45% and 90% of the viewport. Releasing a drag projects the flick velocity to choose the nearest snap point or dismiss, and the backdrop fades with the sheet's progress. It is built on a native <dialog> opened with showModal, so it renders in the top layer, makes the page inert, locks body scroll and returns focus on close. Content scrolls only at the top snap point; below it the content is a drag surface. The handle is a real button that cycles snap points and responds to Arrow keys.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/bottom-sheet.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/bottom-sheet.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/bottom-sheet.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { BottomSheet } from "@/components/ui/bottom-sheet";
import { useState } from "react";

export function BottomSheetExample() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>
        Open sheet
      </button>
      <BottomSheet
        open={open}
        onOpenChange={setOpen}
        title="Trip details"
        snapPoints={[0.45, 0.9]}
      >
        <p>Sheet content goes here.</p>
      </BottomSheet>
    </>
  );
}

03Props

PropTypeDefaultDescription
openboolean—Open state (controlled).
defaultOpenbooleanfalseInitial open state (uncontrolled).
onOpenChange(open: boolean) => void—Called when the sheet asks to open or close.
titleReactNode—Sheet title; labels the dialog.
descriptionReactNode—Supporting text; describes the dialog.
snapPointsnumber[][0.45, 0.9]Snap heights as fractions of the viewport height.
defaultSnapnumber0Index of the snap point the sheet opens to.
dismissiblebooleantrueAllow closing by dragging down or clicking the backdrop. Escape and the close button always close.
backdropOpacitynumber0.5Backdrop opacity at the lowest snap point (0–1).
maxWidthnumber640Maximum sheet width in px.
spring{ stiffness: number; damping: number; mass: number }{ stiffness: 420, damping: 42, mass: 1 }Spring for snapping, opening and closing.
accentstring"#FF7448"Active snap marker and focus ring colour.
handleLabelstring"Resize sheet"Accessible name prefix of the handle button.
closeLabelstring"Close"Accessible name of the close button.
classNamestring—Classes added to the sheet panel.
styleCSSProperties—Inline styles merged onto the sheet panel.
childrenReactNode—Sheet content.

04Features

  • 01Fractional snap points with velocity projection: a flick carries the sheet to the next stop or closes it
  • 02Backdrop opacity is derived from the sheet's position, so it fades as you drag
  • 03Native modal <dialog>: top layer, inert page, Escape, focus return
  • 04Body scroll lock with scrollbar compensation
  • 05Content scrolls only at the top snap point; below it the whole sheet is a drag surface
  • 06Controlled (open + onOpenChange) or uncontrolled (defaultOpen)
motion · springinput · draginput · clickinput · keyboard

05Accessibility

  • Opened with showModal(): the page behind is inert and focus moves into the sheet, then returns to the trigger on close.
  • The dialog is labelled by the title and described by the description.
  • The drag handle is a <button> announcing the current height; activating it cycles snap points, Arrow Up / Down step between them.
  • Escape and a labelled close button always close the sheet, even when drag / backdrop dismissal is turned off.
  • With prefers-reduced-motion the sheet snaps, opens and closes without animation.

06Limitations

  • At the top snap point the sheet is dragged from its header; dragging scrolled content does not move the sheet.