Usage
import { Drawer } from "chakra-ui-solid";<Drawer.Root>
<Drawer.Backdrop />
<Drawer.Trigger />
<Drawer.Positioner>
<Drawer.Content>
<Drawer.CloseTrigger />
<Drawer.Header>
<Drawer.Title />
</Drawer.Header>
<Drawer.Body />
<Drawer.Footer />
</Drawer.Content>
</Drawer.Positioner>
</Drawer.Root>A drawer runs the dialog machine — @zag-js/dialog, the same state machine
Dialog runs, dressed by a different style definition. So every behavioural
prop on Drawer.Root is one of Dialog’s, and createDrawer is createDialog under a second name.
The React version is built the same way.
Examples
Controlled
Use the open and onOpenChange props to control the drawer component.
Sizes
Use the size prop to change the size of the drawer component.
Context
Use the Drawer.Context component to access the drawer state and methods from outside the drawer.
Its render prop is called in the part’s own body, which is not a tracking scope — so it has to return JSX. A callback returning a bare ternary reads the state untracked and freezes on the value it had at mount.
Offset
Use the padding CSS property on Drawer.Positioner to adjust the offset of the drawer component.
Placement
Use the placement prop to change the placement of the drawer component. start and end follow
the writing direction, so end is the right edge in LTR and the left edge in RTL.
Initial Focus
Use the initialFocusEl prop to set the initial focus of the drawer component. It is a function, so
it can read a ref that is still undefined when the drawer is created.
Custom Container
Here’s an example of how to render the drawer component in a custom container.
Consider setting closeOnInteractOutside to false to prevent the drawer from closing when
interacting outside the drawer.
Solid’s Portal takes the container as mount, and reads it once, when the portal is created —
so the container element has to exist by then. Holding it in a signal and gating the portal on that
signal is what guarantees the order; a plain variable can still be undefined at the moment the
portal reads it, and a portal that finds nothing silently attaches to document.body instead.
Render drawer here
Header Actions
Here’s an example of rendering actions in the header of the drawer component.
Drawer with conditional variants
Here is an example of how to change variants based on the different breakpoints.
This example uses the mdDown breakpoint to change the drawer’s placement on smaller screens. This
approach is recommended because both conditions are translated into CSS media queries, which helps
avoid base style merging issues.
If you really want to use the base condition instead, you’ll also need to define corresponding sizes. For example:
<Drawer.Root placement={{ base: "bottom", md: "end" }} size={{ base: "xs", md: "md" }}>placement is a recipe variant, not a style prop, so a responsive value needs the opt-in that
pre-generates it at every breakpoint. Without it the drawer renders with no placement rules at all
and nothing errors:
export default defineChakraConfig({
responsive: { drawer: ["placement"] },
include: [...],
});defineChakraConfig covers the three grains it accepts.
Open drawer and resize screen to mobile size
Non-Modal Drawer
We don’t recommend using a non-modal drawer due to the accessibility concerns they present. In event you need it, here’s what you can do:
- set the
modalprop tofalse - set
pointerEventstononeon theDrawer.Positionercomponent - (optional)set the
closeOnInteractOutsideprop tofalse
Props
Root
| Prop | Default | Type |
|---|---|---|
children | — | JSX.Element |
closeOnEscape | true | booleanWhether Escape closes the dialog. |
closeOnInteractOutside | true | booleanWhether a click outside the content closes the dialog. |
contained | — | ConditionalValue<boolean>Whether the panel is inset from the viewport edges and rounded, rather than flush against them. |
defaultOpen | — | booleanThe open state a fresh, uncontrolled dialog starts in. |
defaultTriggerValue | — | string | nullThe active trigger a fresh, uncontrolled dialog starts with. |
finalFocusEl | — | () => HTMLElement | nullThe element that takes focus when the dialog closes. |
id | — | stringSeeds every id the machine hands out — the content is `dialog:{id}:content`, the trigger `dialog:{id}:trigger`, and so on for all seven parts. Defaults to a generated id. Pass `ids` to name the elements themselves. |
ids | — | DialogElementIdsOverride individual element ids, for pointing an ARIA relationship at a specific one. |
immediate | — | booleanApply an open/close change in the same frame rather than the next one. |
initialFocusEl | — | () => HTMLElement | nullThe element that takes focus when the dialog opens, instead of the content itself. |
lazyMount | true | booleanKeep the content out of the DOM entirely until the drawer first opens. |
modal | true | booleanWhether the content blocks the page behind it — pointer events off, everything else `aria-hidden`. |
onEscapeKeyDown | — | (event: KeyboardEvent) => voidCalled when Escape is pressed, before the dialog decides whether to close. |
onExitComplete | — | VoidFunctionCalled once the exit animation has finished and the content is fully gone. |
onFocusOutside | — | (event: DialogFocusOutsideEvent) => voidCalled when focus moves outside the content. |
onInteractOutside | — | (event: DialogInteractOutsideEvent) => voidCalled on either of the two above. |
onOpenChange | — | (details: DialogOpenChangeDetails) => voidCalled whenever the open state changes, from either side. |
onPointerDownOutside | — | (event: DialogPointerDownOutsideEvent) => voidCalled on a pointer press outside the content. |
onRequestDismiss | — | dialog.Props['onRequestDismiss']Called when a parent layer closing takes this one with it. |
onTriggerValueChange | — | (details: DialogTriggerValueChangeDetails) => voidCalled whenever the active trigger changes. |
open | — | booleanThe controlled open state. Pass `undefined` for uncontrolled. |
persistentElements | — | Array<() => Element | null>Elements that keep their pointer events and do not count as "outside" — a toast, a tour step. |
placement | 'end' | ConditionalValue<'start' | 'end' | 'top' | 'bottom' | PresetVariant<'drawer', 'placement'>>Which edge the panel slides in from. `start` and `end` follow the writing direction, so `end` is the right edge in LTR and the left in RTL. |
present | — | booleanDrive the content's presence from something other than the drawer's own `open` state — an escape hatch for animating the surface independently of the machine. **It reaches the content and the positioner only.** The backdrop builds its own presence straight from `open` and ignores this, so `present={true}` over a closed drawer slides the panel in with no scrim behind it. That asymmetry is Ark's, reproduced rather than corrected. Resolved with `??`, so a wrapper forwarding an unset `present={props.present}` falls back to `open` — parity rather than a fix, since Ark's own split drops an `undefined` before the merge ever sees it. |
preventScroll | true | booleanWhether the page behind the dialog stops scrolling while it is open. |
restoreFocus | — | booleanWhether focus returns to whatever had it before the dialog opened. |
role | 'dialog' | 'dialog' | 'alertdialog'The ARIA role the content carries. `alertdialog` is the interruptive one, for a destructive confirmation. |
size | 'xs' | ConditionalValue<'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full' | PresetVariant<'drawer', 'size'>>How far the panel extends from its edge. `full` covers the viewport. |
skipAnimationOnMount | false | booleanSuppress the enter animation on the very first open, so a `defaultOpen` drawer does not slide in as the page loads. |
trapFocus | true | booleanWhether Tab is confined to the content while it is open. |
triggerValue | — | string | nullThe controlled active trigger, for one dialog shared by several triggers. |
unmountOnExit | true | booleanTake the content back out of the DOM once it has closed and its exit animation has finished, rather than leaving it there hidden. |
Plus the three every component takes: as, render and unstyled.