Drawer
A panel that slides in from an edge of the screen and closes with a swipe, for touch-first layouts.
<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
DrawerClosebutton 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.
| Prop | Type | Default |
|---|---|---|
actionsRef | RefObject<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. | - |
defaultOpen | booleanWhether the drawer is initially open. | false |
defaultSnapPoint | DrawerSnapPoint | nullThe initial snap point value when uncontrolled. | - |
defaultTriggerId | string | nullID of the trigger that the drawer is associated with. This is useful in conjunction with the `defaultOpen` prop to create an initially open drawer. | - |
disablePointerDismissal | booleanWhether 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 |
handle | DrawerHandle<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. | - |
modal | boolean | "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. | - |
open | booleanWhether the drawer is currently open. | - |
showSwipeHandle | booleanShows a grab bar on the drawer's inner edge. Defaults to false. | false |
snapPoint | DrawerSnapPoint | nullThe currently active snap point. Use with `onSnapPointChange` to control the snap point. | - |
snapPoints | DrawerSnapPoint[]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'`). | - |
snapToSequentialPoints | booleanDisables 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 |
triggerId | string | nullID 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.
| Prop | Type | Default |
|---|---|---|
container | HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | nullA parent element to render the portal element into. | - |
keepMounted | booleanWhether to keep the portal mounted in the DOM while the popup is hidden. | false |
render | ReactElement<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.
| Prop | Type | Default |
|---|---|---|
forceRender | booleanWhether the backdrop is forced to render even when nested. | false |
render | ReactElement<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
| Prop | Type | Default |
|---|---|---|
handle | DrawerHandle<unknown>A handle to associate the trigger with a drawer. Can be created with the Drawer.createHandle() method. | - |
id | stringID 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). | - |
nativeButton | booleanWhether 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 |
payload | unknownA payload to pass to the drawer when it is opened. | - |
render | ReactElement<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
| Prop | Type | Default |
|---|---|---|
nativeButton | booleanWhether 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 |
render | ReactElement<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.
| Prop | Type | Default |
|---|---|---|
finalFocus | boolean | RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)Determines the element to focus when the drawer is closed. | - |
initialFocus | boolean | RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null)Determines the element to focus when the drawer is opened. | - |
render | ReactElement<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
| Prop | Type | Default |
|---|---|---|
render | ReactElement<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
| Prop | Type | Default |
|---|---|---|
render | ReactElement<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. | - |