Drawer

Used to render a content that slides in from the side of the screen

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:

panda.config.ts
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 modal prop to false
  • set pointerEvents to none on the Drawer.Positioner component
  • (optional)set the closeOnInteractOutside prop to false

Props

Root

DrawerRootProps
PropDefaultType
children—
JSX.Element
closeOnEscapetrue
boolean

Whether Escape closes the dialog.

closeOnInteractOutsidetrue
boolean

Whether 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—
boolean

The open state a fresh, uncontrolled dialog starts in.

defaultTriggerValue—
string | null

The active trigger a fresh, uncontrolled dialog starts with.

finalFocusEl—
() => HTMLElement | null

The element that takes focus when the dialog closes.

id—
string

Seeds 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—
DialogElementIds

Override individual element ids, for pointing an ARIA relationship at a specific one.

immediate—
boolean

Apply an open/close change in the same frame rather than the next one.

initialFocusEl—
() => HTMLElement | null

The element that takes focus when the dialog opens, instead of the content itself.

lazyMounttrue
boolean

Keep the content out of the DOM entirely until the drawer first opens.

modaltrue
boolean

Whether the content blocks the page behind it — pointer events off, everything else `aria-hidden`.

onEscapeKeyDown—
(event: KeyboardEvent) => void

Called when Escape is pressed, before the dialog decides whether to close.

onExitComplete—
VoidFunction

Called once the exit animation has finished and the content is fully gone.

onFocusOutside—
(event: DialogFocusOutsideEvent) => void

Called when focus moves outside the content.

onInteractOutside—
(event: DialogInteractOutsideEvent) => void

Called on either of the two above.

onOpenChange—
(details: DialogOpenChangeDetails) => void

Called whenever the open state changes, from either side.

onPointerDownOutside—
(event: DialogPointerDownOutsideEvent) => void

Called 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) => void

Called whenever the active trigger changes.

open—
boolean

The 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—
boolean

Drive 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.

preventScrolltrue
boolean

Whether the page behind the dialog stops scrolling while it is open.

restoreFocus—
boolean

Whether 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.

skipAnimationOnMountfalse
boolean

Suppress the enter animation on the very first open, so a `defaultOpen` drawer does not slide in as the page loads.

trapFocustrue
boolean

Whether Tab is confined to the content while it is open.

triggerValue—
string | null

The controlled active trigger, for one dialog shared by several triggers.

unmountOnExittrue
boolean

Take 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.