Overlays & feedback
Drawer
A side sheet that slides in over the current view to show or edit a record without leaving it — the detail panel for a task, a contact, a settings group. Open it from any row, change fields in place, and dismiss with the scrim, the close button, or Escape.
npx shadcn@latest add @orbit/drawerTask detail
The drawer keeps the list in view behind a scrim, so changes feel attached to where they came from. Pick any row to open its detail, Mark complete in the footer — the row behind updates at once — or dismiss with the scrim, the ×, or Esc. The open state is controlled: the list owns which task is showing.
Anatomy
A fixed header and footer bracket a scrolling body. The header carries the title, an optional description, and the close ×; the body scrolls between them; the footer holds the primary action. All three are slots — header from title/description, body from children, footer from footer.
Sides
Enter from either edge. Right is the default — detail panels read naturally from a list. The hairline border and shadow sit on the inner edge, and swipe-to-dismiss follows the side.
Sizes
sm 360px for quick edits, md 440px (default) for record detail, lg 560px for dense forms. Every size caps at 92% of the viewport, and width accepts a custom value when the scale doesn't fit.
Scrim
scrim=false drops the overlay for a non-modal panel: the page behind stays fully interactive and outside clicks don't dismiss it — close with the × or Escape. Use it when the panel is a companion to the view, not an interruption.
Controlled
Pass open and onOpenChange to own the state — required when rows, keyboard shortcuts, or routes decide what the drawer shows. Leave them off and pass trigger (or defaultOpen) for uncontrolled use.
open: false
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| side | "right" | "left" | "right" | Edge the panel enters from. Sets the slide direction, inner-edge border, and swipe direction. |
| size | "sm" | "md" | "lg" | "md" | Panel width: 360 / 440 / 560px, capped at 92% of the viewport. |
| width | string | — | Custom panel width (e.g. “500px”) — overrides size. |
| scrim | boolean | true | false removes the overlay: non-modal, the page stays interactive, and outside clicks don't dismiss. |
| open | boolean | — | Controlled open state. Pair with onOpenChange. |
| defaultOpen | boolean | false | Initial open state when uncontrolled. |
| onOpenChange | (open: boolean) => void | — | Called when the drawer opens or closes — trigger, scrim, ×, Esc, or swipe. |
| trigger | ReactElement | — | Element that opens the drawer — merged into a Base UI Trigger (a Button works as-is). |
| title | ReactNode | — | Heading in the fixed header, announced as the dialog's accessible name. |
| description | ReactNode | — | Supporting line under the title, linked via aria-describedby. |
| footer | ReactNode | — | Sticky bar below the body — actions live here. Reuse Button. |
| children | ReactNode | — | Body content — scrolls between the fixed header and footer. |
| …rest | HTMLAttributes | — | className and everything else is forwarded to the panel element, including ref. |
Usage
Use it to preserve context
When the user is working a list and wants detail without losing their scroll position or filters, a drawer keeps both.
Stack drawers on drawers
A second panel over the first buries the trail. Replace the contents, or route to a full page for deep navigation.
Commit edits immediately
Inline fields should save on change — no Save button to forget. Show the result in the list behind the scrim.
Hide a blocking decision in it
A drawer is dismissible by design. For a choice the user must make before continuing, use a Modal instead.
Keyboard & accessibility
Base UI provides the dialog role, focus trap, and scroll lock. The title labels the panel via aria-labelledby, the description is linked via aria-describedby, Escape and the scrim dismiss, focus returns to the trigger on close, and the slide animation respects prefers-reduced-motion.