Skip to content
UI

Drawer

A panel that slides in from an edge of the screen and closes with a swipe, for touch-first layouts.

@zenoxai/ui/components/drawerBase UI Markdown
<Drawer showSwipeHandle>
  <DrawerTrigger render={<Button variant="outline" />}>
    Triage alert
  </DrawerTrigger>
  <DrawerContent>
    <div className="mx-auto w-full max-w-md">
      <DrawerHeader>
        <DrawerTitle>Triage alert</DrawerTitle>
        <DrawerDescription>
          Brute-force login attempt on vpn-gw-02.prod
        </DrawerDescription>
      </DrawerHeader>
      <p className="p-4 text-text-secondary">
        214 failed logins from 3 addresses in the last 10 minutes. The
        account was not locked.
      </p>
      <DrawerFooter>
        <Button>Escalate to incident</Button>
        <DrawerClose render={<Button variant="outline" />}>
          Mark as false positive
        </DrawerClose>
      </DrawerFooter>
    </div>
  </DrawerContent>
</Drawer>

Usage

import { Button } from "@zenoxai/ui/components/button"
import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@zenoxai/ui/components/drawer"
<Drawer showSwipeHandle>
  <DrawerTrigger render={<Button variant="outline" />}>
    Triage alert
  </DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Triage alert</DrawerTitle>
      <DrawerDescription>
        Brute-force login attempt on vpn-gw-02.prod
      </DrawerDescription>
    </DrawerHeader>
    <div className="p-4">{/* body */}</div>
    <DrawerFooter>
      <Button>Escalate to incident</Button>
      <DrawerClose render={<Button variant="outline" />}>Close</DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>

A drawer is a dialog with gestures. The user can drag it to close it, snap it between heights and stack one on top of another. Use it where people work with their thumbs: mobile layouts, bottom sheets with quick actions, a summary that expands to full height.

  • On desktop, a panel that slides in from the side and needs no gestures is a Sheet. Prefer it there: it is simpler and sized for forms.
  • For a short task in the middle of the screen, use Dialog.
  • To confirm a destructive action, use Alert Dialog.

showSwipeHandle draws the grab bar. Show it whenever the drawer can be swiped, so the gesture is discoverable. DrawerFooter stacks its buttons full width, primary action first. Give the body p-4; without a body, the footer keeps the same 1rem gap below the header by itself. A bottom drawer spans the whole viewport width; on wide screens, wrap its content in mx-auto w-full max-w-md so lines and buttons stay readable.

Examples

Direction

swipeDirection is the direction that dismisses the drawer, and so the edge it enters from: down (the default, a bottom sheet), up, left or right. Side drawers take three quarters of the width on small screens and 24rem from the sm breakpoint up.

<>
  {(["up", "right", "down", "left"] as const).map((direction) => (
    <Drawer key={direction} swipeDirection={direction} showSwipeHandle>
      <DrawerTrigger render={<Button variant="outline" />}>
        Swipe {direction}
      </DrawerTrigger>
      <DrawerContent>
        {/* flex-1 flex-col keeps the footer at the bottom of side drawers;
            max-w-md keeps bottom and top drawers readable on wide screens. */}
        <div className="mx-auto flex w-full max-w-md flex-1 flex-col">
          <DrawerHeader>
            <DrawerTitle>Swipe {direction} to close</DrawerTitle>
            <DrawerDescription>
              The drawer enters from the edge it is dismissed toward.
            </DrawerDescription>
          </DrawerHeader>
          <DrawerFooter>
            <DrawerClose render={<Button variant="outline" />}>
              Close
            </DrawerClose>
          </DrawerFooter>
        </div>
      </DrawerContent>
    </Drawer>
  ))}
</>

Snap points

snapPoints lets a bottom drawer rest at preset heights. Numbers from 0 to 1 are fractions of the viewport height, larger numbers are pixels, and strings accept px and rem. The drawer opens at the first point. Control the current point with snapPoint and onSnapPointChange.

<Drawer snapPoints={["10rem", 1]} showSwipeHandle>
  <DrawerTrigger render={<Button variant="outline" />}>
    Open asset summary
  </DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>srv-db-01.prod</DrawerTitle>
      <DrawerDescription>
        Drag up for the full summary, down to close.
      </DrawerDescription>
    </DrawerHeader>
    <dl className="mx-auto grid w-full max-w-md grid-cols-2 gap-x-4 gap-y-3 p-4">
      <dt className="text-text-secondary">IP address</dt>
      <dd>10.20.4.17</dd>
      <dt className="text-text-secondary">Operating system</dt>
      <dd>Ubuntu 22.04 LTS</dd>
      <dt className="text-text-secondary">Tenant</dt>
      <dd>Acme Corp</dd>
      <dt className="text-text-secondary">Open alerts</dt>
      <dd>3</dd>
      <dt className="text-text-secondary">Last seen</dt>
      <dd>2 minutes ago</dd>
    </dl>
  </DrawerContent>
</Drawer>

Nested

A drawer opened from inside another stacks on top of it; the one behind scales down and dims. Use it for a confirmation or a second step that belongs to the first drawer.

<Drawer showSwipeHandle>
  <DrawerTrigger render={<Button variant="outline" />}>
    Alert details
  </DrawerTrigger>
  <DrawerContent>
    <div className="mx-auto w-full max-w-md">
      <DrawerHeader>
        <DrawerTitle>Malware detected</DrawerTitle>
        <DrawerDescription>ws-finance-07, Acme Corp</DrawerDescription>
      </DrawerHeader>
      <DrawerFooter>
        <Drawer showSwipeHandle>
          <DrawerTrigger render={<Button />}>Isolate host</DrawerTrigger>
          <DrawerContent>
            <div className="mx-auto w-full max-w-md">
              <DrawerHeader>
                <DrawerTitle>Isolate ws-finance-07?</DrawerTitle>
                <DrawerDescription>
                  The host loses network access except to the Zenox agent.
                </DrawerDescription>
              </DrawerHeader>
              <DrawerFooter>
                <DrawerClose render={<Button variant="destructive" />}>
                  Isolate host
                </DrawerClose>
                <DrawerClose render={<Button variant="outline" />}>
                  Cancel
                </DrawerClose>
              </DrawerFooter>
            </div>
          </DrawerContent>
        </Drawer>
        <DrawerClose render={<Button variant="outline" />}>
          Close
        </DrawerClose>
      </DrawerFooter>
    </div>
  </DrawerContent>
</Drawer>

Accessibility

  • The drawer is modal by default: focus moves into it and stays there, Escape and a click on the backdrop close it, and focus returns to the trigger. modal={false} lets the user keep working with the page behind it.
  • Swiping is never the only way out. Keep a DrawerClose button in the footer for keyboard and screen reader users.
  • Render a DrawerTitle: it is the drawer’s accessible name. The swipe handle is hidden from assistive technology.

API reference

Generated from the TypeScript source. className, style and children are accepted everywhere and omitted below.

Drawer

Drawer is a panel that slides in from an edge and can be swiped closed, made for touch: a bottom sheet of actions or a short form on mobile. On desktop, use Sheet for a side panel and Dialog for a centered window.

PropTypeDefault
actionsRefRefObject<DrawerRootActions | null>

A ref to imperative actions. - `unmount`: Ends the closing phase of the drawer after an externally controlled closing animation finishes. Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the drawer completes closing on its own. Whether it leaves the DOM is decided by `keepMounted` on the portal. - `close`: Closes the drawer imperatively when called.

-
defaultOpenboolean

Whether the drawer is initially open.

false
defaultSnapPointDrawerSnapPoint | null

The initial snap point value when uncontrolled.

-
defaultTriggerIdstring | null

ID of the trigger that the drawer is associated with. This is useful in conjunction with the `defaultOpen` prop to create an initially open drawer.

-
disablePointerDismissalboolean

Whether to prevent the drawer from closing on outside presses. For non-modal drawers, this also prevents the drawer from closing when focus moves outside of it.

false
handleDrawerHandle<unknown>

A handle to associate the drawer with a trigger. If specified, allows detached triggers to control the drawer's open state. Can be created with the Drawer.createHandle() method.

-
modalboolean | "trap-focus"

Determines if the drawer enters a modal state when open. - `true`: user interaction is limited to just the drawer: focus is trapped, document page scroll is locked, and pointer interactions on outside elements are disabled. - `false`: user interaction with the rest of the document is allowed. - `'trap-focus'`: focus is trapped inside the drawer, but document page scroll is not locked and pointer interactions outside of it remain enabled.

true
onOpenChange((open: boolean, eventDetails: DrawerRootChangeEventDetails) => void)

Event handler called when the drawer is opened or closed.

-
onOpenChangeComplete((open: boolean) => void)

Event handler called after any animations complete when the drawer is opened or closed.

-
onSnapPointChange((snapPoint: DrawerSnapPoint | null, eventDetails: DrawerRootSnapPointChangeEventDetails) => void)

Callback fired when the snap point changes.

-
openboolean

Whether the drawer is currently open.

-
showSwipeHandleboolean

Shows a grab bar on the drawer's inner edge. Defaults to false.

false
snapPointDrawerSnapPoint | null

The currently active snap point. Use with `onSnapPointChange` to control the snap point.

-
snapPointsDrawerSnapPoint[]

Snap points used to position the drawer. Use numbers between 0 and 1 to represent fractions of the viewport height, numbers greater than 1 as pixel values, or strings in `px`/`rem` units (for example, `'148px'` or `'30rem'`).

-
snapToSequentialPointsboolean

Disables velocity-based snap skipping so drag distance determines the next snap point.

false
swipeDirection"left" | "right" | "up" | "down"

The swipe direction used to dismiss the drawer.

down
triggerIdstring | null

ID of the trigger that the drawer is associated with. This is useful in conjunction with the `open` prop to create a controlled drawer. There's no need to specify this prop when the drawer is uncontrolled (that is, when the `open` prop is not set).

-

DrawerPortal

DrawerPortal moves the drawer to the end of the document body. DrawerContent renders it already.

PropTypeDefault
containerHTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null

A parent element to render the portal element into.

-
keepMountedboolean

Whether to keep the portal mounted in the DOM while the popup is hidden.

false
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerPortalState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-

DrawerOverlay

DrawerOverlay is the backdrop, which fades as the drawer is swiped away. DrawerContent renders it already when the drawer is modal.

PropTypeDefault
forceRenderboolean

Whether the backdrop is forced to render even when nested.

false
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerBackdropState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-

DrawerSwipeHandle

DrawerSwipeHandle is the grab bar, hidden from screen readers. Drawer renders it when `showSwipeHandle` is set.

No props of its own. Accepts the props of the element it renders.

DrawerTrigger

PropTypeDefault
handleDrawerHandle<unknown>

A handle to associate the trigger with a drawer. Can be created with the Drawer.createHandle() method.

-
idstring

ID of the trigger. In addition to being forwarded to the rendered element, it is also used to specify the active trigger for drawers in controlled mode (with the Drawer.Root `triggerId` prop).

-
nativeButtonboolean

Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).

true
payloadunknown

A payload to pass to the drawer when it is opened.

-
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerTriggerState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-

DrawerClose

PropTypeDefault
nativeButtonboolean

Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).

true
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerCloseState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-

DrawerContent

DrawerContent is the sliding panel, in a portal with a backdrop when the drawer is modal. The edge it comes from follows `swipeDirection` on Drawer: `down`, the default, makes a bottom sheet.

PropTypeDefault
finalFocusboolean | RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)

Determines the element to focus when the drawer is closed.

-
initialFocusboolean | RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null)

Determines the element to focus when the drawer is opened.

-
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerPopupState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-

DrawerHeader

No props of its own. Accepts the props of the element it renders.

DrawerFooter

DrawerFooter holds the actions at the bottom, stacked at full width. A body between header and footer brings its own 16px inset (`p-4`); when the footer follows the header directly, it adds the 16px gap itself.

No props of its own. Accepts the props of the element it renders.

DrawerTitle

PropTypeDefault
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerTitleState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-

DrawerDescription

PropTypeDefault
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerDescriptionState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-