A side panel that slides in from the top, right, bottom, or left edge of the viewport. Built on @radix-ui/react-dialog so it ships with focus trapping, Escape-to-dismiss, and role="dialog" for free. Pick Sheet when you want a plain edge-anchored panel — for the same behaviour with swipe-to-dismiss physics and snap points, reach for Drawer instead.
Sheet is a compound — Root owns open state, Trigger opens it, Content is the panel (auto-portaled with its own overlay, and accepts the side prop), Header stacks Title and Description, Footer pins actions to the bottom, and Close dismisses it.
import { Sheet } from "@craft-bits/core";<Sheet.Root> <Sheet.Trigger asChild> <button>Open settings</button> </Sheet.Trigger> <Sheet.Content side="right"> <Sheet.Header> <Sheet.Title>Settings</Sheet.Title> <Sheet.Description> Configure how the app behaves on this device. </Sheet.Description> </Sheet.Header> <Sheet.Footer> <Sheet.Close>Done</Sheet.Close> </Sheet.Footer> </Sheet.Content></Sheet.Root>
Drive the open state from your own store by passing open plus onOpenChange:
Sheet vs. Drawer vs. Dialog. A Dialog is centered and modal; a Sheet is edge-anchored without gestures; a Drawer is edge-anchored with vaul's swipe-to-dismiss physics. Reach for Sheet when the content has a natural "out of the way" position but you do not want the mobile-sheet gesture stack — for example a desktop settings panel, a side nav rail, or an edge-anchored detail view.
Compound parts. Root (state owner), Trigger (opens the sheet, accepts asChild), Overlay (backdrop scrim), Content (the panel; accepts side; auto-wraps in Portal and Overlay unless you pass withPortal={false}), Header and Footer (layout helpers), Title and Description (Radix-wired aria-labelledby and aria-describedby), Close (the dismiss trigger).
The side prop lives on Content.side="right" (default) and side="left" slide in horizontally and cap their width at sm:max-w-sm. side="top" and side="bottom" stretch full-width and slide in vertically. Each side animates from its own edge — open and close use slide-in-from-* and slide-out-to-* Tailwind animation utilities keyed off the panel's data-state.
Controlled or uncontrolled. Omit open and onOpenChange to let Radix manage state. Pass both to drive open/close from your own state — useful for triggering the sheet from a remote action (keyboard shortcut, async callback, deep link).
asChild on Trigger and Close. Both accept asChild — drop in your own Button primitive (or a framework Link for navigation-on-close) and the sheet's event handlers merge onto it via Radix's Slot.
Compose your own portal. Pass withPortal={false} to Content to skip the auto-portal and place the overlay yourself — handy when you need to mount the panel inside a specific stacking context.
Props
Sheet.Root
Prop
Type
Default
Description
open
boolean
—
Controlled open state. Pair with onOpenChange.
onOpenChange
(open: boolean) => void
—
Fires when the sheet opens or closes.
defaultOpen
boolean
false
Uncontrolled initial open state.
modal
boolean
true
When true, interaction outside the sheet is blocked.
Sheet.Trigger
Prop
Type
Default
Description
asChild
boolean
false
Render trigger props onto the child element via Radix's Slot.
className
string
—
Merged onto the rendered element.
Sheet.Content
Prop
Type
Default
Description
side
"top" | "right" | "bottom" | "left"
"right"
Edge the panel slides out from.
withPortal
boolean
true
Auto-wrap in Portal and Overlay. Set false to compose them yourself.
overlayClassName
string
—
Merged onto the auto-rendered overlay (only when withPortal is true).
className
string
—
Merged onto the rendered panel.
Sheet.Header / Sheet.Footer
Prop
Type
Default
Description
className
string
—
Merged onto the rendered <div>.
Sheet.Title / Sheet.Description
Prop
Type
Default
Description
className
string
—
Merged onto the rendered element.
Sheet.Close
Prop
Type
Default
Description
asChild
boolean
false
Render close props onto the child element via Radix's Slot.
className
string
—
Merged onto the rendered button.
Accessibility
Radix renders the panel as role="dialog" and traps focus inside until the sheet closes.
Title is wired as the panel's accessible name (aria-labelledby); Description is wired as its description (aria-describedby). Both are required by Radix — wrap them in sr-only if your visual design has none.
Escape and backdrop clicks dismiss the sheet (override via Radix's onEscapeKeyDown and onPointerDownOutside on Content).
The trigger receives focus back when the sheet closes so keyboard users return to where they were.
Animations honour prefers-reduced-motion via the data-[state=*]:animate-in / animate-out Tailwind classes (pair with tw-animate-css, which respects the OS setting).
Credits
Extracted from: algoflashcards (src/platform/ui/sheet.tsx). The original embedded a hardcoded close button + XIcon and used the project's font-heading token; craft-bits drops the auto-close button (consumers wire their own via Sheet.Close) and re-skins every surface with cb-* tokens so it themes alongside the rest of the library.