- Layer
- Machine
- Package
@grassroot/ui-machines/sheet- Ships behaviour
- Identical to Sheet's: open binding, dismissable guard. No Drawer-specific state.
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.
@grassroot/ui-machines/sheet@grassroot/ui-headless-{react,vue,solid,svelte,angular}/drawerdata-scope="drawer", four-way data-side.@grassroot/ui-headless-core/drawer (mountDrawer)@grassroot/ui-{react,vue,solid,svelte,angular}- Layer
- Headless
- Package
@grassroot/ui-headless-{react,vue,solid,svelte,angular}/drawer- Ships behaviour
- Dialog-shaped modal DOM/ARIA under
data-scope="drawer", four-waydata-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.
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
-
drawer.contract.ts, the shared browser-contract suite, implemented per adapter in each framework's ownbrowser/drawer.spec.ts(React, Vue, Solid, Svelte, Angular) plus the core vanilla mount. -
drawer.spec.md, the styling-differences note. It explicitly defers anatomy, S12 binding rules, dismissal reasons, keyboard behaviour, manager lifecycle, commands, and SSR rules tosheet.spec.md. Code must match both, and where either disagrees with code, the spec wins. -
Build your own component on
@grassroot/ui-headless-core, the same machine → part scope → connect → commands recipe, applied to a component that isn't one of the shipped machines. - Sheet: the full shared behaviour contract, worked through from Sheet's own side domain
- Dialog: the modal sibling whose manager contract Sheet and Drawer both reuse