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#

TSX
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#

TSX
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#

TSX
<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)#

TSX
<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#

PartElementStatesFlagsNotes
triggerbuttonopen | closeddisabled, focus-visible, pressed, press-animatingaria-haspopup="dialog", aria-expanded, aria-controls. Carries the variant axes. asChild.
paneldialogopen | closeddata-placement from start | end; aria-labelledby while a Title renders, else aria-label from label.
backdroppseudoopen | closedRenders no element on the web; projects onto panel::backdrop.
titleh2Inside panel.
closebuttondisabled, focus-visible, pressed, press-animatingCloses 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#

PropTypeDefaultDescription
modelbooleanTwo-way binding of the open state.
defaultOpenbooleanfalseInitial state when uncontrolled.
openChangeevent (open: boolean)Fires whenever the open state changes.
modalbooleantrueshowModal() (focus trap, inert background, backdrop) or inline show().
dismissiblebooleantrueWhether 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.
labelstringAccessible name of the panel when no Drawer.Title renders.

Drawer.Trigger#

PropTypeDefaultDescription
disabledbooleanfalseRenders data-disabled.
color / size / variant / axes / modsdesign-system vocabularyThe variant axes for the scope, rendered on the trigger.
asChildbooleanfalseRender through the default slot, which receives the part bag.
classstringExtra classes.

Drawer.Panel, Drawer.Title#

Only class.

Drawer.Close#

PropTypeDefaultDescription
disabledbooleanfalseRenders data-disabled.
asChildbooleanfalseRender through the default slot.
classstringExtra classes.

Keyboard and dismissal#

InteractionModalInline (modal={false})
EscapeCloses, via the native cancel event, when dismissible.Closes through zero's dismiss layer, when dismissible.
Scrim clickCloses 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.
TabTrapped inside the panel by the platform.Moves freely between the panel and the page.
Focus on closeRestored 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 (xsxl) 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.

Dialog for a centred modal on the same machinery · Popover for an anchored, non-modal surface.