Docs Platform

Drawer

Drawer is Sheet's machine wearing a different styled recipe, a wider set of sides, and the legacy bottom grab handle. There is no second implementation of a slide-in panel underneath. If you have read Sheet's page you already know Drawer's behaviour contract, so this page only covers what is actually different.

Explore the layers

Machine is @grassroot/ui-machines/sheet, the identical package Sheet uses, not a Drawer-specific one. Headless (@grassroot/ui-headless-<framework>/drawer) applies the same Dialog-shaped anatomy under data-scope="drawer" instead of "sheet". Styled (@grassroot/ui-<framework>) is Grassroot's legacy Drawer visual recipe (eyebrow, title, bottom grab handle and all) rebuilt on the shared headless layer. Vanilla (mountDrawer, from @grassroot/ui-headless-core/drawer) is Drawer's own framework-free mount helper; it wires up the same machine and the same activateDrawerManagers call Sheet's activateSheetManagers mirrors.

Layer
Package
Ships behaviour
Machine
@grassroot/ui-machines/sheet
Identical to Sheet's: open binding, dismissable guard. No Drawer-specific state.
Headless
@grassroot/ui-headless-{react,vue,solid,svelte,angular}/drawer
Dialog-shaped modal DOM/ARIA under data-scope="drawer", four-way data-side.
Vanilla
@grassroot/ui-headless-core/drawer (mountDrawer)
The same headless contract with no framework runtime at all.
Styled
@grassroot/ui-{react,vue,solid,svelte,angular}
Grassroot's legacy Drawer recipe: eyebrow/title slots and the bottom grab handle.
Layer
Machine
Package
@grassroot/ui-machines/sheet
Ships behaviour
Identical to Sheet's: open binding, dismissable guard. No Drawer-specific state.
Layer
Headless
Package
@grassroot/ui-headless-{react,vue,solid,svelte,angular}/drawer
Ships behaviour
Dialog-shaped modal DOM/ARIA under data-scope="drawer", four-way data-side.
Layer
Vanilla
Package
@grassroot/ui-headless-core/drawer (mountDrawer)
Ships behaviour
The same headless contract with no framework runtime at all.
Layer
Styled
Package
@grassroot/ui-{react,vue,solid,svelte,angular}
Ships behaviour
Grassroot's legacy Drawer recipe: eyebrow/title slots and the bottom grab handle.

One machine, two components

Everything in Sheet's behaviour contract and managers & dismissal section applies to Drawer verbatim: the same open/defaultOpen binding, the same dismissable guard defaulting to true, the same modal layer pushed through activateDrawerManagers (Sheet's sibling call to activateSheetManagers), the same ref-counted focus trap and scroll lock, and the same OPEN_CHANGE_REQUEST command. There is no separate binding rule, no separate dismissal rule, and no separate S12 mode-flip diagnostic for Drawer. It is the same machine, configured through the same input.

What differs is entirely in the connector's styling metadata and the styled layer's legacy surface, covered below.

Anatomy & side domain

Drawer exposes the identical Dialog-shaped parts Sheet does: Root, Trigger, Backdrop, Positioner, Content, Title, Description, CloseTrigger, all under data-scope="drawer" instead of "sheet". Render rules, unmount-on-close behaviour, and the conditional aria-labelledby/aria-describedby wiring are all identical; see Sheet's anatomy table for the full part list.

The one real difference is the side domain. Sheet accepts only "left" | "right", defaulting to "right". Drawer accepts "bottom" | "top" | "left" | "right", defaulting to "bottom", which is Drawer's larger, legacy-compatible set. In both components side is connector-only styling metadata: it never enters machine state, and it never changes a transition. The connector emits it as data-side either way.

Drawer's legacy grab handle: when side === "bottom", Drawer's styled layer renders the legacy bottom grab handle with aria-hidden="true". Sheet never renders this handle at all. It isn't part of Sheet's visual recipe, and it isn't machine or headless behaviour in either component; it is a styling-layer-only concern that reads the same side input the connector already exposes.

Drawer's styled layer also preserves its legacy public surface exactly: open, onOpenChange, side, eyebrow, title, children, dismissable, and className in the React/Solid forms, with the equivalent named/default slots and projected title markers in Vue/Svelte/Angular.

Keyboard, pointer, focus

Identical to Sheet's: Trigger click opens with reason "trigger", CloseTrigger click closes with reason "close-trigger", the layer's Escape and outside-pointerdown dismissal fire the matching close reason when the drawer is dismissable, Tab/Shift+Tab wrap inside Content, and focus returns to whatever was active when the managers were activated. See Sheet's keyboard table for the full row-by-row contract. Nothing here is Drawer-specific.

Accessibility

Content carries role="dialog" and aria-modal="true", exactly like Sheet's. The bottom grab handle is aria-hidden="true" so it never intrudes on the accessible name or description; it is a purely visual affordance for pointer and touch users. Everything else (the conditional label/description wiring, the ref-counted scroll lock, and the Tab-trap) is Sheet's manager-owned behaviour, reused without modification; see Sheet's accessibility section.

Evidence & further reading