Drawer
The edge panel — navigation, filters, a cart — on the same native <dialog>
machinery as Dialog: the top layer instead of a Portal, showModal() for focus trapping,
Escape and focus restore, and a ::backdrop pseudo part. What is the drawer's own is the
edge: the panel declares data-placement="start|end" from the logical pair, so a navigation
drawer sits at the reading start in both directions and RTL mirrors for free.
Import
import { Drawer } from '@sigx/zero/drawer';
Drawer is a compound: Drawer.Root, Drawer.Trigger, Drawer.Panel, Drawer.Title,
Drawer.Close. It is also re-exported from the @sigx/zero root, together with
drawerAnatomy, useDrawerContext and the DrawerPlacement type.
Usage
import { component } from 'sigx';
import { Drawer } from '@sigx/zero/drawer';
const SiteNav = component(({ signal }) => {
const state = signal({ open: false });
return () => (
<Drawer.Root model={() => state.open} label="Site navigation">
<Drawer.Trigger color="neutral">Menu</Drawer.Trigger>
<Drawer.Panel>
<Drawer.Title>Navigation</Drawer.Title>
<nav>…</nav>
<Drawer.Close>Close</Drawer.Close>
</Drawer.Panel>
</Drawer.Root>
);
});
model={() => state.open} binds the open state both ways. Leave the model off and pass
defaultOpen to keep the state inside the component; openChange fires either way. See
Models. Drawer.Root renders no element of its own — it only provides
context — so the trigger and the panel can sit anywhere in the tree below it. The panel is
rendered closed in place and lifted into the top layer when it opens; there is no Portal and
no z-index to manage.
The end edge
<Drawer.Root model={() => state.cart} placement="end" label="Shopping cart">
placement is the logical pair start (default) or end, stamped as data-placement on the
panel. A recipe pins the panel with inset-inline-start: 0 / inset-inline-end: 0 — never
left / right — and the layout mirrors under dir="rtl" with no further rule.
Inline (non-modal)
<Drawer.Root model={() => state.filters} modal={false}>
modal={false} opens with show() instead of showModal(): the panel renders in flow, the
page beside it stays interactive, and there is no backdrop. Clicks elsewhere are a non-event —
an inline drawer is furniture that survives them — while Escape still closes it, under
dismissible, through zero's dismiss layer. show() provides no focus restore either, so
zero covers that itself on close.
Naming a drawer without a heading
A drawer often has no visible heading. label on the root becomes the panel's aria-label
whenever no Drawer.Title is rendered; while a title is present, the panel is labelled by it
instead and label is not rendered. Give every drawer one or the other.
Anatomy
| Part | Element | States | Flags | Notes |
|---|---|---|---|---|
trigger | button | open | closed | disabled, focus-visible, pressed, press-animating | aria-haspopup="dialog", aria-expanded, aria-controls. Carries the variant axes. asChild. |
panel | dialog | open | closed | — | data-placement from start | end; aria-labelledby while a Title renders, else aria-label from label. |
backdrop | pseudo | open | closed | — | Renders no element on the web; projects onto panel::backdrop. |
title | h2 | — | — | Inside panel. |
close | button | — | disabled, focus-visible, pressed, press-animating | Closes the drawer. Inside panel. asChild. |
Drawer's Root renders a fragment, so there is no root part. The trigger is the carrier
part: the variant axes live on Drawer.Trigger, and a design system's axis rules for the
scope anchor there. The panel is a top-layer sibling of the trigger, so colour styles the
trigger, never the panel.
The backdrop is a real part that renders no element on the web: recipes style
parts.backdrop and the compiler projects it onto
[data-scope="drawer"][data-part="panel"]::backdrop, with states narrowing the panel. Modal
and inline need no attribute of their own to tell apart — :modal is the platform's spelling
of exactly that split, so a recipe writes &:modal for the trapped, scrimmed presentation
and &:not(:modal) for the in-flow one. Both close and trigger publish
press feedback. See
The anatomy contract.
Props
Drawer.Root
| Prop | Type | Default | Description |
|---|---|---|---|
model | boolean | — | Two-way binding of the open state. |
defaultOpen | boolean | false | Initial state when uncontrolled. |
openChange | event (open: boolean) | — | Fires whenever the open state changes. |
modal | boolean | true | showModal() (focus trap, inert background, backdrop) or inline show(). |
dismissible | boolean | true | Whether Escape and, for a modal drawer, a scrim click close it. |
placement | 'start' | 'end' | 'start' | Which reading edge the panel sits on; rendered as data-placement on the panel. |
label | string | — | Accessible name of the panel when no Drawer.Title renders. |
Drawer.Trigger
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | false | Renders data-disabled. |
color / size / variant / axes / mods | design-system vocabulary | — | The variant axes for the scope, rendered on the trigger. |
asChild | boolean | false | Render through the default slot, which receives the part bag. |
class | string | — | Extra classes. |
Drawer.Panel, Drawer.Title
Only class.
Drawer.Close
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | false | Renders data-disabled. |
asChild | boolean | false | Render through the default slot. |
class | string | — | Extra classes. |
Keyboard and dismissal
| Interaction | Modal | Inline (modal={false}) |
|---|---|---|
| Escape | Closes, via the native cancel event, when dismissible. | Closes through zero's dismiss layer, when dismissible. |
| Scrim click | Closes when dismissible; only a pointer outside the panel's box counts, so a click on the panel's own padding does not. | No backdrop; clicks elsewhere are not an event. |
| Tab | Trapped inside the panel by the platform. | Moves freely between the panel and the page. |
| Focus on close | Restored by the platform. | Restored by zero. |
In the shipped design systems
@sigx/zero-basic and @sigx/zero-daisyui both wire color (the eight recommended roles)
and size (xs–xl) on the drawer trigger, in each skin's own button idiom. Neither
wires a variant or any mods on the scope. The panel, backdrop, title and close parts are
styled by every shipped design system, including the slide transition keyed on
data-placement and the :modal scrim.
Related
Dialog for a centred modal on the same machinery ·
Popover for an anchored, non-modal surface.
