Sheet
Sheet slides a panel in from the edge of the screen over the page. Use it for side tasks such as filters, an outline or the AI Tutor on phones.
Beta until design adds a drawer or bottom sheet to Figma. The component is code-only and reuses Modal's overlay, header, footer and close button; the drag handle follows the live app's AI Tutor sheet. Everything on this page is implemented and tested.
Preview
Change the controls, then open the sheet. Press Escape, select the overlay, use a button or swipe the handle to close it.
import {
Button, Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger,
} from '@nuton/ui'
<Sheet>
<SheetTrigger asChild><Button appearance="stroke">Course outline</Button></SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Biology 101</SheetTitle>
<SheetDescription>8 lessons · 3 completed</SheetDescription>
</SheetHeader>
<SheetBody>
<ol>{lessons.map((l) => <li key={l}>{l}</li>)}</ol>
</SheetBody>
<SheetFooter>
<SheetClose asChild><Button appearance="stroke">Close</Button></SheetClose>
<SheetClose asChild><Button onClick={resume}>Resume lesson</Button></SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>Installation
Availability
@nuton/ui and @nuton/tokens are private workspace packages and aren't published yet (D4, D12). Inside the Nuton monorepo, add them as workspace dependencies.
pnpm add @nuton/ui @nuton/tokens @remixicon/reactnpm install @nuton/ui @nuton/tokens @remixicon/reactyarn add @nuton/ui @nuton/tokens @remixicon/reactimport '@nuton/tokens/tokens.css'
import '@nuton/ui/styles.css'Usage
import { Button, Sheet, SheetBody, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@nuton/ui'
<Sheet>
<SheetTrigger asChild><Button appearance="stroke">Filters</Button></SheetTrigger>
<SheetContent side="right" size="sm" aria-describedby={undefined}>
<SheetHeader><SheetTitle>Filter lessons</SheetTitle></SheetHeader>
<SheetBody>…</SheetBody>
</SheetContent>
</Sheet>- Use a side sheet for supporting tasks people do next to the page: filters, an outline, details of an item.
- Use a bottom sheet on phones for things that would be a side panel on wider screens, such as the AI Tutor.
- Bottom sheets have a drag handle: swipe down to close, or press it. Side sheets can add one with
handle. Keep the close button too; the gesture is never the only way out. - Every sheet needs a
SheetTitle. Add aSheetDescription, or passaria-describedby={undefined}. - For a short decision, use a Modal. For a small preview next to a word, use a Popover.
Variants and sizes
Three sides and three sizes. Right and left follow the reading direction.
Sides
<SheetContent side="left">…</SheetContent>
<SheetContent side="right">…</SheetContent>
<SheetContent side="bottom">…</SheetContent>Drag handle and snap points
<SheetContent side="bottom">…</SheetContent> {/* handle on by default */}
<SheetContent side="bottom" snapPoints={[0.5, 1]}>…</SheetContent>
<SheetContent side="right" handle>…</SheetContent>Sizes and measurements
| Part | Measurement |
|---|---|
| Side sheet (left, right) | Full height; width sm 360 · md 480 · lg 640, never wider than the screen; 1px Stroke/Soft-200 on the inner edge, no radius |
| Bottom sheet | Full width up to 664, centred; top corners radius 24; height fits content up to sm 50% · md 75% · lg 100% of the screen minus 16 |
| Header · footer bars | 20 above and below, 24 at the sides, 1px dividers (as Modal); 16 and 20 below 480px |
| Body | 24 padding, items 12 apart; the only part that scrolls |
| Close | Button neutral lighter, small, icon-only (36 × 36), 20 from the end, centred on the title |
| Drag handle | Bar 36 × 4, fully rounded, State/Faded/Base (Icon/Sub-600 on hover, focus and drag), 12 from the edge. Button target 48 × 28 at the top of a bottom sheet (which gains a 16 top zone), 28 × 48 on the inner edge of a side sheet |
States and interactions
| State | Behaviour |
|---|---|
| Opening | Slides in from its edge over the base duration with the emphasized curve; the overlay fades in. Focus moves to the first control. |
| Open | Focus is trapped inside; page scroll is locked; the rest of the page is hidden from assistive technology. |
| Closing | Escape, the overlay, the close button or a SheetClose. Slides out over the fast duration; focus returns to the trigger. |
| Dragging | With a handle, drag the handle or the header (and, on a bottom sheet, the body when it is scrolled to the top) towards the sheet's edge. The sheet follows the finger; data-dragging is set. Pulling the other way is damped and stops within 48. |
| Released | Past 25% of the height (bottom) or width (side), or a quick flick faster than 0.5 px/ms, closes it. Otherwise it springs back over the slow duration (data-settling). |
| Scrolled body | Dragging inside a scrolled body scrolls it. The sheet only moves once the body is back at the top. Text fields never start a drag; add data-sheet-no-drag to opt out anything else. |
| Snap points | With snapPoints, the bottom sheet rests at those fractions of its maximum height (data-snap holds the index). A release snaps to the nearest point, a flick moves one point, and pulling more than 25% below the smallest closes it. |
| Reduced motion | No slide, fade or spring: the sheet appears, disappears and snaps back at once. Swiping still closes it. |
State tokens
Set on .nu-sheet: --nu-sheet-width, --nu-sheet-bottom-width, --nu-sheet-bottom-max, --nu-sheet-gap, --nu-sheet-close-size, --nu-sheet-pad-inline, --nu-sheet-bar-pad, --nu-sheet-radius, --nu-sheet-handle-length, --nu-sheet-handle-thickness, --nu-sheet-handle-inset, --nu-sheet-handle-zone, --nu-sheet-handle-hit-long, --nu-sheet-handle-hit-short and --nu-sheet-handle-color. The component sets --nu-sheet-offset and --nu-sheet-snap while dragging.
Code-only component
Figma has no drawer or bottom sheet. Widths, the bottom sheet radius and heights, the inner-edge border, the motion and the swipe thresholds are code-only and need design review. The handle size matches the live app and an archived Figma handle; its colour is darker than that archive so it meets 3:1. Swipe behaviour is tested with simulated pointers; check it on real phones before release.
Composition examples
AI Tutor on phones
On phones the AI Tutor opens at half height with the lesson visible above. Drag the handle up (or press it) for the full conversation; swipe down to close. The message field never starts a drag.
Filters
Course outline
API reference
SheetContent props
| Prop | Type | Default | Description |
|---|---|---|---|
side | 'right' | 'left' | 'bottom' | 'right' | Edge it slides in from. Right and left swap in right-to-left pages. |
size | 'sm' | 'md' | 'lg' | 'md' | Side: width 360 / 480 / 640. Bottom: maximum height 50% / 75% / 100%. |
showClose | boolean | true | Shows the close button. Without it, add a SheetClose in the footer. |
closeLabel | string | 'Close' | Accessible name of the close button. |
handle | boolean | true for side='bottom', otherwise false | Shows the drag handle button and turns on swipe-to-close. |
handleLabel | string | 'Close sheet' ('Resize sheet' with snap points) | Accessible name of the handle button. |
snapPoints | number[] | — | Bottom sheets with a handle: resting heights as fractions of the maximum height, smallest first, e.g. [0.5, 1]. |
snapPoint | number | — | Current snap point index (controlled). |
defaultSnapPoint | number | 0 | Snap point index the sheet opens at (uncontrolled). |
onSnapPointChange | (index: number) => void | — | Called when the sheet snaps to another point. |
container | HTMLElement | — | Portal container. Defaults to document.body. |
…Radix props | Dialog.Content | — | aria-describedby, onEscapeKeyDown, onOpenAutoFocus and others. The ref points at the panel. |
Sheet takes open, defaultOpen and onOpenChange. SheetHeader, SheetBody and SheetFooter are divs; SheetTitle renders an h2 and SheetDescription a paragraph. SheetTrigger and SheetClose take asChild.
Accessibility
Keyboard
| Key | Behaviour |
|---|---|
| Tab / Shift + Tab | Moves through the sheet; focus stays inside |
| Esc | Closes the sheet and returns focus to the trigger |
| Enter / Space | On the handle: closes the sheet, or with snap points moves to the next height |
| ↑ / ↓ | On the handle with snap points: taller or shorter |
Screen readers
A modal dialog named by SheetTitle and described by SheetDescription. The page behind is hidden from assistive technology while it is open. The handle is a real button named “Close sheet” (or “Resize sheet”), so the swipe has an equivalent that needs no gesture.
Focus
Focus moves to the first control on open (the handle and close button come after the content in the DOM), is trapped while open, and returns to the trigger on close. The handle shows a 2px focus ring.
Contrast and colour
Title 17.93:1 and description 6.69:1 on white. The handle bar is 4.2:1 (6.69:1 on hover and focus). Forced-colours mode draws a system border around the sheet and draws the handle in the text colour.
Touch and pointer
The close button is 36 × 36; the handle target is 48 × 28. A tap on the overlay closes the sheet. Swipe-to-close is a single-pointer drag, never the only way to close (WCAG 2.5.1, 2.5.7): the handle button, close button and Escape all work. The bottom sheet keeps clear of the phone's home indicator (safe-area inset).
You must: give every sheet a title, and keep a visible way to close it when showClose is off.
Guidelines and related components
Use Sheet for
- Filters and sort options
- A course outline or table of contents
- The AI Tutor and other side tools on phones
Don't use Sheet for
- Short confirmations (use Modal)
- Small previews (use Popover)
- Main navigation that should always be visible
Do and don't
Phone: AI Tutor as side="bottom"
Phone: AI Tutor as side="right" size="lg"