Noctis

A panel that slides in from a screen edge and can be dragged or flicked away. The draggable sibling of Sheet — a trigger opens a portalled surface that you can swipe to dismiss, snap between preset heights, stack in layers, or open with an edge gesture.

Basic

A Drawer.Trigger opens the Drawer.Panel — the all-in-one portal, backdrop, viewport, and draggable popup. Compose the trigger from a Button through render. Inside, a Drawer.Grip gives the panel a visible drag affordance, and a swipe-safe Drawer.Content holds a Drawer.Header (with a Title and Description), a scrollable Drawer.Body, and a Drawer.Footer whose actions close the panel through Drawer.Close. Unlike a dialog there is no corner close button — drawers are dismissed by swiping, the grip, an outside press, or Escape.

Sides & sizes

Dock the panel to any edge with side on Drawer.Rootbottom (the default), top, start, or end. side is root-owned: the root maps it to the dismiss-swipe direction (start/end flip under RTL) and seeds the descendants, so the viewport and popup position themselves with no extra wiring — and the panel can never render on a different edge than it swipes toward. size on the panel sets the cross-axis extent — sm, md (default), lg, xl, or full — the width cap for side drawers and the height cap for top and bottom, so every size is meaningful on every edge (full lifts the cap).

Snap points

Pass snapPoints to Drawer.Root to make a bottom sheet snap between preset heights — a compact peek and a near full-height view. Points are fractions of the viewport (01), pixel numbers (> 1), or px/rem strings; the full-height 1 stop drives data-expanded. A fast flick can skip past stops; snapToSequentialPoints forces distance-based snapping instead.

Nested drawers

Render a Drawer.Root inside another drawer's Drawer.Content to stack them — here three levels deep. Each parent scales back and lifts toward its edge as the next opens, an iOS-style card stack that handles each panel's variable height. Focus and Escape ordering stay correct across the stack, and clicking the dimmed area dismisses the frontmost panel first, peeling the stack back one level at a time. A nested drawer keeps a transparent backdrop as that click-catcher (the visible dim comes from the root drawer's scrim), and each parent's transform reads the live nesting count and frontmost height.

Action sheet

A bottom sheet works well as an iOS-style action list: a group of Drawer.Close-backed actions that each dismiss the sheet, with a destructive action and a cancel separated into the footer.

Mobile navigation

A full-height start drawer makes a compact navigation menu — a scrollable list of links, each closing the drawer on selection. Swipe toward the start edge to dismiss.

Swipe to open

A Drawer.SwipeArea is an invisible edge strip that opens the drawer when you swipe inward — no visible trigger needed. It sits as a sibling of Drawer.Portal inside Drawer.Root, anchors itself at the docked edge, and pairs with modal={false} so the page stays interactive. Here it is scoped to a card by portalling into a local container.

Swipe inward from the docked edge — or use the button — to open the drawer.

Indent effect

Wrap the app shell in a Drawer.Provider with a Drawer.Indent (and a Drawer.IndentBackground behind it) to make the whole UI scale back and reveal a darker layer when a drawer opens — the "the app sinks back" effect. Portal the drawer into the indented region and use modal={false} so it sits in front of the receding shell; the scale tracks the live drag.

Opening the drawer scales this panel back, revealing the layer behind it.

Close confirmation

For a drawer with unsaved work, control its open state and call eventDetails.cancel() inside onOpenChange to veto a close, then ask for confirmation through an Alert dialog. Confirming discards the work and closes the drawer for real.

Detached triggers

Triggers don't have to live inside Drawer.Root. Create a shared handle with Drawer.createHandle(), pass it to the root and to any Drawer.Trigger anywhere in the tree, and give each trigger a payload — it arrives through the root's function child, so one drawer can render different content per trigger.

Theme scope

ThemeScope re-tunes a whole region's shape — corner radius, density, and type scale — through the cascade, with no per-component prop and without touching the app-wide theme. Tweak the knobs below, then open the drawer: the panel re-tunes too, even though it portals to <body>, because the scope rides a portal-safe context. See Customization for nesting, reset, and the raw-token portal caveat.

Keyboard

KeyAction
Enter / SpaceOn the trigger: open the drawer and move focus into the panel.
Tab / Shift + TabMove focus to the next / previous element, trapped within the open panel (when modal).
EscClose the panel and return focus to the trigger; in a stack, pop the top drawer first.

A click on the dimmed backdrop also closes the drawer, and on touch devices a swipe toward the docked edge dismisses it. Dismissal is vetoable through onOpenChange's cancel().

Anatomy

Compose a drawer from its parts. Drawer.Root owns the open state and the docked side (it accepts every Base UI Drawer.Root prop — open, defaultOpen, onOpenChange, modal, snapPoints, swipeDirection, handle). Modal by default: focus is trapped, the page is scroll-locked, and the rest of the document is inert.

  • Drawer.Root — owns the open state, the docked side, and snap/swipe behaviour; renders no element of its own.
  • Drawer.Trigger — opens the drawer. Style it directly or compose a Button through render; pass handle/payload for detached control.
  • Drawer.Panel — the common composition: portal, backdrop, viewport, and the draggable popup in one. Props: side (defaults from the root), size (default md), backdropClassName, container, and forceRenderBackdrop. Reach for Drawer.Portal + Backdrop + Viewport + Popup directly to customize the portal container or backdrop wiring.
  • Drawer.Grip — a decorative drag-handle pill; place it directly under the popup (outside Content) so it stays a drag target.
  • Drawer.Content — the swipe-safe wrapper for selectable text, scrollable lists, and controls, so pointer interactions there aren't read as dismiss swipes.
  • Drawer.Header — the top region for the Title, Description, and corner actions, separated from the body by a divider.
  • Drawer.Body — the scrollable middle region; it grows to fill and scrolls its overflow so the header and footer stay put.
  • Drawer.Footer — the bottom region, pinned to the base, typically holding the primary and secondary actions.
  • Drawer.Title + Drawer.Description — the panel's accessible name and supporting copy, linked to the popup via aria-labelledby and aria-describedby.
  • Drawer.Close — closes the nearest drawer. A bare button with no styling of its own, so it composes with any Button through render — a footer action, or an optional ghost icon in the header gutter. Drawers dismiss primarily by swipe, the grip, an outside press, or Escape, so an explicit close is optional.
  • Drawer.SwipeArea — an invisible edge strip that opens the drawer on an inward swipe.
  • Drawer.Provider / Drawer.Indent / Drawer.IndentBackground — coordinate the app-shell indent effect.

Every rendered part carries a data-slot (noctis-drawer-trigger, noctis-drawer-backdrop, noctis-drawer-viewport, noctis-drawer-popup, noctis-drawer-grip, noctis-drawer-content, noctis-drawer-header, noctis-drawer-body, noctis-drawer-footer, noctis-drawer-title, noctis-drawer-description, noctis-drawer-close, noctis-drawer-swipe-area, noctis-drawer-indent, noctis-drawer-indent-background) for host-side styling — the viewport and popup also carry data-side, and the popup data-size. Pair them with the Base UI state attributes (data-open, data-closed, data-starting-style, data-ending-style, data-swiping, data-swipe-direction, data-expanded, data-nested-drawer-open, data-nested). The popup renders through Surface at elevated elevation, so controls inside re-derive off that base and separate cleanly.

Design tokens

Generated from the component's declaration — the same graph that mints the CSS, so a variable name or its resolution default can't drift. The minted tokens are the public override seam: set one on any ancestor and every drawer in that region retunes — e.g. .app { --noctis-drawer-popup-peek-offset: 1.5rem; } widens how far stacked panels fan out beneath it. Knobs that aren't minted are reached through the part's data-slot. See Customization for the full override ladder and Tokens for the whole graph.

Token

API reference

Generated from the component's types — every prop, type, default, and description comes straight from the source. Each part gets its own table; parts that only forward to Base UI's Drawer list just the props they pass through. Expand a row for the full type and description.

Drawer.Root

Prop

Drawer.Trigger

Prop

Drawer.Portal

Prop

Drawer.Backdrop

Prop

Drawer.Viewport

Prop

Drawer.Popup

Prop

Drawer.Panel

Prop

Drawer.Grip

Prop

Drawer.Content

Prop

Drawer.Header

Prop

Drawer.Body

Prop

Drawer.Footer

Prop

Drawer.Title

Prop

Drawer.Description

Prop

Drawer.Close

Prop

Drawer.SwipeArea

Prop

Drawer.Provider

No props of its own — forwards to the underlying Base UI part.

Drawer.Indent

Prop

Drawer.IndentBackground

Prop

AttributeDescription
data-slotThe slot anchor every rendered part carries.
data-sideOn the viewport and popup: which edge the drawer docks to (`start` · `end` · `top` · `bottom`).
data-sizeOn the popup: the cross-axis size step (`sm` · `md` · `lg` · `xl` · `full`).
data-openPresent while the drawer is open.
data-closedPresent while the drawer is closed.
data-starting-stylePresent while the drawer is animating in.
data-ending-stylePresent while the drawer is animating out.
data-swipingPresent while the drawer is being dragged.
data-swipe-dismissPresent on the popup once a swipe crosses the dismiss threshold.
data-swipe-directionThe active swipe axis/direction (`up` · `down` · `left` · `right`), on the popup and swipe area.
data-expandedPresent on the popup at the full-height (`1`) snap point.
data-nested-drawer-openPresent on a parent popup while a nested drawer is open.
data-nested-drawer-swipingPresent on a parent popup while a nested drawer is being dragged.
data-nestedPresent on a viewport/popup that is nested inside another drawer.
data-activePresent on the indent and indent-background while any drawer in the provider is open.
data-disabledPresent on a disabled trigger, close, or swipe area.