Sheet
A panel that slides in from an edge of the window over a dimmed page: a side panel on a laptop and a drawer on a phone.
import { Sheet } from "@butteryui/react";Demo
Slides in at the window's end, 400px wide.
When to use it
Use it for
- A shop's bag or cart, beside the page on a laptop and from the bottom on a phone
- Filters or settings for the page behind, longer than a dialog holds
- A record's details opened from a list, with its actions kept in view
Not for
- A decision or a short task that needs full attention: use Dialog
- A panel that should sit beside the page and narrow it rather than cover it, such as help: use SidePanel
- A few options under a button: use Menu or Popover
- A step that deserves its own address, such as checking out: use a page
Examples
Cart
A shop's bag: at the window's end on a laptop and rising from the bottom on a phone, its total and Check out in view.
Filter panel
Filters in a drawer on a phone and at the window's start from 640px, opened and closed from state the list keeps.
Guji, Huila, Halfmoon House, Night Owl, Kirinyaga
Settings
Settings in a sheet at the window's end: each switch takes effect as it's turned, and Done closes it.
Prices with tax, shipping only
Sides and sizes
A sheet from each side, and the two wider sizes a side sheet takes.
Variants and states
Sides
It slides in from its edge on spring.smooth over a scrim that fades in over dur-base, and leaves over dur-fast. At the bottom it's a drawer with a handle to drag it down by.
A drawer on a phone, a side panel from 640px
With side={{ base: "bottom", md: "end" }} one sheet rises from the bottom of a narrow window and slides in at the end of a wider one.
Sizes
A side sheet is 400, 560 or 880px wide, the widths a dialog has, and never wider than the window less 16px.
Props
Sheet
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | Whether it's open. Pair with onOpenChange to control it. | |
defaultOpen | boolean | false | Whether it starts open when uncontrolled. Defaults to false. |
onOpenChange | ((open: boolean) => void) | Called when it opens or closes: its trigger, a close button, Escape, a press on the scrim or a drawer dragged down. | |
children | ReactNode | The trigger and the content. |
Sheet.Body
It also takes ComponentPropsWithRef<"div">.
Sheet.Close
It also takes ComponentPropsWithRef<typeof RadixDialog.Close>.
Sheet.Content
| Prop | Type | Default | Description |
|---|---|---|---|
side | "end" | "start" | "bottom" | ResponsiveSheetSide | "end" | The edge it slides in from: "end" (the default), "start" or "bottom", or a side for each width,
such as { base: "bottom", md: "end" }, a drawer on a phone and a side panel from 640px. |
size | "sm" | "md" | "lg" | "sm" | How wide it is at the end or start: "sm" (400px, the default), "md" (560px) or "lg" (880px), never
wider than the window less 16px. At the bottom it takes the window's width whatever its size. |
label | string | Names the sheet for screen readers when it has no Sheet.Header title to name it. | |
description | ReactNode | Describes the sheet for screen readers when it has no Sheet.Header description to describe it. | |
showClose | boolean | true | Whether it shows a close button in its top corner. Defaults to true. |
closeLabel | string | "Close" | Names the close button. Defaults to "Close". |
closeDisabled | boolean | false | Shows the close button disabled (aria-disabled, still focusable) and stops it closing the sheet, such
as while a save is pending. Defaults to false. |
It also takes Omit<ComponentPropsWithRef<typeof RadixDialog.Content>, "forceMount" | "asChild">.
Sheet.Footer
It also takes ComponentPropsWithRef<"div">.
Sheet.Header
| Prop | Type | Default | Description |
|---|---|---|---|
titlerequired | ReactNode | The sheet's title, which names it for screen readers. | |
description | ReactNode | A line under the title, such as how many things it holds, which describes the sheet for screen readers. |
It also takes Omit<ComponentPropsWithRef<"div">, "title" | "children">.
Sheet.Trigger
It also takes ComponentPropsWithRef<typeof RadixDialog.Trigger>.
ResponsiveSheetSide
Sides that change with the window's width: `base` below 640px, `md` from 640 and `lg` from 1024 (the page's breakpoints). A width left out keeps the side below it, such as `{ base: "bottom", md: "end" }` for a drawer on a phone that's a side panel from 640px.
| Field | Type | Default | Description |
|---|---|---|---|
base | "end" | "start" | "bottom" | "end" | The side below 640px. Defaults to "end". |
md | "end" | "start" | "bottom" | The side from 640px. | |
lg | "end" | "start" | "bottom" | The side from 1024px. |
Accessibility
A modal dialog (aria-modal) named by its Sheet.Header title, or by label without one, and described by the header's description. Focus waits on the sheet while it slides in, then moves to its first field to type in, or stays on the sheet when it has none, so Tab starts at its first control. Tab stays inside; Escape, the close button, a press on the scrim or a drawer dragged down closes it, and focus goes back to what opened it. The page behind is hidden from screen readers while it's open. A drawer's handle only decorates: its close button and Escape do what dragging does.
| Keys | What they do |
|---|---|
| Enter or Space | Opens it from its trigger |
| Tab or Shift+Tab | Moves to the next or previous control inside it, going round so focus never leaves it |
| Escape | Closes it and hands focus back to what opened it |
Motion
It appears with a `Presence` preset and leaves faster than it came, in about 60% of the time.
With reduced motion: It and its scrim fade in and out over 100ms where it rests, without sliding; a drawer dragged short of closing lands back at once.
Layout
Sheet.Content sits 8px inside the window. At the end or start it's as tall as the window less 16px and 400px wide at size="sm", the default, 560 at "md" and 880 at "lg", never wider than the window less 16px. At the bottom it takes the window's width less 16px and its content's height, up to the window less 64px, with a 24px handle strip across its top. side takes a side for each width, { base, md, lg }, changing at 640 and 1024px of window. Sheet.Header and Sheet.Footer keep their size while Sheet.Body scrolls between them; the footer stacks its children 12px apart at its full width. The close button is a 28px IconButton centered 24px in from the top and end edges, and the title keeps clear of it.
Related
See it in the Halfmoon shop demo.