Installation
Theme
Mode

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/drawer

Task 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.

Tasks5Select a row to open detail

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.

title + description header · scrolling body · sticky 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.

side=right (default) · side=left

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.

scrim=false — non-modal

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

PropTypeDefaultDescription
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.
widthstringCustom panel width (e.g. “500px”) — overrides size.
scrimbooleantruefalse removes the overlay: non-modal, the page stays interactive, and outside clicks don't dismiss.
openbooleanControlled open state. Pair with onOpenChange.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => voidCalled when the drawer opens or closes — trigger, scrim, ×, Esc, or swipe.
triggerReactElementElement that opens the drawer — merged into a Base UI Trigger (a Button works as-is).
titleReactNodeHeading in the fixed header, announced as the dialog's accessible name.
descriptionReactNodeSupporting line under the title, linked via aria-describedby.
footerReactNodeSticky bar below the body — actions live here. Reuse Button.
childrenReactNodeBody content — scrolls between the fixed header and footer.
…restHTMLAttributesclassName and everything else is forwarded to the panel element, including ref.

Usage

Do

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.

Don’t

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.

Do

Commit edits immediately

Inline fields should save on change — no Save button to forget. Show the result in the list behind the scrim.

Don’t

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.

Keys
EscClose the drawerClick scrimClose the drawerTab / Shift+TabCycle focus inside the open panelSwipeDrag toward the edge to dismiss (touch)roledialog semantics via Base UIFocusTrapped while open, returned to the trigger on close