Drawer
A modal panel that slides in from an edge of the viewport, for details or a task beside the page, with a header, a body that scrolls and a footer. side="bottom" is a bottom sheet.
Import
import { Drawer } from '@nizivo/react/drawer';
// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css and @nizivo/react/css/icon-button.css
import '@nizivo/react/css/drawer.css';Static or interactive
Client. It is the Dialog's engine at an edge: @nizivo/react/drawer measures 31.7 KiB gzip with
React Aria inside, almost all of it shared with Dialog (size-limit, all locales;
packages/react/SIZES.md). How to choose.
Sides
Drawer.Popup takes side:
end(the default): the right edge in a left-to-right page, the left edge in a right-to-left one. This is Figma's Drawer: 440 px wide, the full height, rounded on its two inner corners.start: the opposite edge.bottom: a bottom sheet, the full width and as tall as its content, rounded on its top corners. It is the "More" sheet AppShell will open below 640 px.
data-side on the popup (and its positioner) is the physical edge it sits at: right, left or
bottom. A narrow viewport keeps a 3rem strip of the scrim beside (or above) the drawer, so there is
always somewhere to press to close it. Set --nz-drawer-width for another width, and
--nz-drawer-radius and --nz-drawer-bg to restyle the panel.
Motion
The drawer slides its whole width (or height) in from its edge on duration/slow with
easing/enter, and out on duration/base with easing/exit; the scrim only fades. Under reduced
motion (prefers-reduced-motion: reduce or [data-nz-motion="reduce"]) it does not slide: it only
fades, in 120 ms.
Parts
Drawer.Header holds the title and, under it, a description or badges, above a rule.
Drawer.Body is the only scroller, and fills the height between the header and the footer.
Drawer.Footer holds the actions, which share its width, above a rule.
<Drawer.Close /> with no render and no children is an IconButton (tertiary, md) showing the
close glyph, named "Close". Placed as a direct child of Drawer.Popup, it sits in the header's end
corner, where Figma draws the close glyph, and the header leaves room for it. With children it is a
plain button; with render, your element, for example render={<Button>Cancel</Button>}.
Focus return
As the Dialog's: returnFocus on Drawer.Root (an element ref or a function that returns one, while
it is on the page), else the trigger, else the trigger of the overlay the drawer was opened from,
else <body> with a development warning. returnFocus={false} leaves focus alone.
Inside other overlays
A Drawer opened from a Dialog, or a Dialog opened from a Drawer, adds no second scrim; the one
underneath is marked data-nested-open. A Popover, Select, Menu or Tooltip opened inside a Drawer
portals into it and paints above it. Escape and a press outside close only the topmost overlay, and
the page stays scroll-locked until the last one closes.
Examples
Basic
Figma's Drawer (756:1001): an order's details from the end edge, with a header, a body that scrolls, and a footer whose two actions share its width. The bare Drawer.Close is the header's close IconButton, named "Close".
import type { JSX } from 'react';
import { Badge } from '@nizivo/react/badge';
import { Button } from '@nizivo/react/button';
import { Drawer } from '@nizivo/react/drawer';
export function DrawerExample(): JSX.Element {
return (
<Drawer.Root>
<Drawer.Trigger render={<Button variant="secondary">View order</Button>} />
<Drawer.Popup>
<Drawer.Header>
<Drawer.Title>Order #1042</Drawer.Title>
<Drawer.Description>
<Badge tone="success">Paid</Badge> <Badge tone="warning">Unfulfilled</Badge>
</Drawer.Description>
</Drawer.Header>
<Drawer.Close />
<Drawer.Body>
<p>2 × Stoneware mug, 1 × Linen tea towel.</p>
<p>Ship to Asha Rao, 14 Lake Road, Pune.</p>
</Drawer.Body>
<Drawer.Footer>
<Button>Print label</Button>
<Button variant="primary">Fulfil order</Button>
</Drawer.Footer>
</Drawer.Popup>
</Drawer.Root>
);
}
Sheet
The bottom sheet, as AppShell's "More" sheet below 640 px: as tall as its content, the full width, open on the server (closed and server-open overlays both emit no popup HTML, §12.2 SSR).
Source only
defaultOpen), which would move focus on this page, so it is shown as source only.import type { JSX } from 'react';
import { Button } from '@nizivo/react/button';
import { Drawer } from '@nizivo/react/drawer';
export function DrawerSheetExample(): JSX.Element {
return (
<Drawer.Root defaultOpen>
<Drawer.Trigger render={<Button variant="secondary">More</Button>} />
<Drawer.Popup side="bottom">
<Drawer.Header>
<Drawer.Title>More</Drawer.Title>
</Drawer.Header>
<Drawer.Close />
<Drawer.Body>
<a href="#marketing">Marketing</a>
<a href="#discounts">Discounts</a>
<a href="#settings">Settings</a>
</Drawer.Body>
</Drawer.Popup>
</Drawer.Root>
);
}
Parts
Use the parts through the namespace (Drawer.Root) or by their flat names (DrawerRoot); both work from Server Components.
Drawer.Root
Client component Renders no element of its own. Props: DrawerRootProps. Source: src/drawer/drawer.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | ReactNode | – | |
defaultOpen | boolean | – | |
onOpenChange | (open: boolean, details: ChangeDetails<OpenReason>) => void | – |
|
open | boolean | – | |
returnFocus | ReturnFocusTarget | true | Where focus goes when the drawer closes (§12.2 Focus return), as |
Drawer.Trigger
Without render, its own <button> with nz-hit-target and, from drawer.css, a 24 x 24 px minimum (WCAG 2.2 2.5.8, as the Dialog's, M27). A composed Button or IconButton keeps its own size.
Client component Renders data-slot="drawer-trigger" with the class nz-drawer-trigger. Props: DrawerTriggerProps. Source: src/drawer/drawer.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
children | ReactNode | – | |
render | RenderProp<DrawerTriggerState> | – | Must render a |
- Also takes every attribute of
<button>exceptchildren. (Omit<ComponentPropsWithRef<"button">, "children">)
render takes an element, or, in client code, a function of the props and this state (DrawerTriggerState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – | |
focusVisiblerequired | boolean | – | |
openrequired | boolean | – | |
pressedrequired | boolean | – |
Drawer.Popup
Client component Renders data-slot="drawer-popup" with the class nz-drawer-popup. Props: DrawerPopupProps. Source: src/drawer/drawer.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
backdrop | boolean | – | The scrim element. Defaults to on for the root modal and off when opened from another modal. |
childrenrequired | ReactNode | – | |
container | Element | null | – | Portal container. Defaults to the parent overlay, else |
side | DrawerSide"start""end""bottom" | 'end' | The edge it slides in from. |
- Also takes every attribute of
<div>exceptchildren,role. (Omit<ComponentPropsWithRef<"div">, "children" | "role">)
Drawer.Header
Figma's Header frame: the title, an optional description and badges, above a 1 px rule.
Client component Renders data-slot="drawer-header" with the class nz-drawer-header. Props: DrawerHeaderProps. Source: src/drawer/drawer.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<Record<string, never>> | – |
- Also takes every attribute of
<div>. (ComponentPropsWithRef<"div">)
render takes an element, or, in client code, a function of the props and no state.
Drawer.Title
Client component Renders data-slot="drawer-title" with the class nz-drawer-title. Props: DrawerTitleProps. Source: src/drawer/drawer.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
level | DrawerTitleLevel23456 | 2 | |
render | RenderProp<Record<string, never>> | – |
- Also takes every attribute of
<h2>exceptid. (Omit<ComponentPropsWithRef<"h2">, "id">)
render takes an element, or, in client code, a function of the props and no state.
Drawer.Description
Client component Renders data-slot="drawer-description" with the class nz-drawer-description. Props: DrawerDescriptionProps. Source: src/drawer/drawer.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<Record<string, never>> | – |
- Also takes every attribute of
<p>exceptid. (Omit<ComponentPropsWithRef<"p">, "id">)
render takes an element, or, in client code, a function of the props and no state.
Drawer.Body
The only scroller inside the drawer; it fills the height between the header and the footer.
Client component Renders data-slot="drawer-body" with the class nz-drawer-body. Props: DrawerBodyProps. Source: src/drawer/drawer.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<Record<string, never>> | – |
- Also takes every attribute of
<div>. (ComponentPropsWithRef<"div">)
render takes an element, or, in client code, a function of the props and no state.
Drawer.Footer
Figma's Footer frame: actions that share its width, above a 1 px rule.
Client component Renders data-slot="drawer-footer" with the class nz-drawer-footer. Props: DrawerFooterProps. Source: src/drawer/drawer.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
render | RenderProp<Record<string, never>> | – |
- Also takes every attribute of
<div>. (ComponentPropsWithRef<"div">)
render takes an element, or, in client code, a function of the props and no state.
Drawer.Close
With render or children, the consumer's button. With neither, an IconButton (tertiary, md) showing the core close glyph, named by labels.close: the IconButton element's markup written out, as Dialog.Close does, so the Drawer entry does not pull in IconButton's tooltip chain.
Client component Renders data-slot="drawer-close" with the class nz-drawer-close. Props: DrawerCloseProps. Source: src/drawer/drawer.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
children | ReactNode | – | Text content, rendered in a plain |
labels | Partial<Pick<DrawerLabels, "close">> | – | The default IconButton's accessible name; an |
render | RenderProp<DrawerCloseState> | – | Must render a |
- Also takes every attribute of
<button>exceptchildren. (Omit<ComponentPropsWithRef<"button">, "children">)
render takes an element, or, in client code, a function of the props and this state (DrawerCloseState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – | |
focusVisiblerequired | boolean | – | |
pressedrequired | boolean | – |
Other exports
DrawerBodyPropstypeimport type { DrawerBodyProps } from '@nizivo/react/drawer'The props of
Drawer.Body, listed above.DrawerClosePropstypeimport type { DrawerCloseProps } from '@nizivo/react/drawer'The props of
Drawer.Close, listed above.DrawerCloseStatetypeimport type { DrawerCloseState } from '@nizivo/react/drawer'The render state of
Drawer.Close, listed above.DrawerDescriptionPropstypeimport type { DrawerDescriptionProps } from '@nizivo/react/drawer'The props of
Drawer.Description, listed above.DrawerFooterPropstypeimport type { DrawerFooterProps } from '@nizivo/react/drawer'The props of
Drawer.Footer, listed above.DrawerHeaderPropstypeimport type { DrawerHeaderProps } from '@nizivo/react/drawer'The props of
Drawer.Header, listed above.DrawerLabelstypeimport type { DrawerLabels } from '@nizivo/react/drawer'Strings the Drawer renders (§9.3 rule 7, §16.4).
closeis the accessible name of the defaultDrawer.Close(an IconButton showing the close glyph), set through that part'slabels. Engine-owned with React Aria (not overridable):dismiss, the visually hidden "Dismiss" button rendered inside the drawer.Name Type Default Description closerequiredstring'Close'dismissrequiredstring– DrawerPopupPropstypeimport type { DrawerPopupProps } from '@nizivo/react/drawer'The props of
Drawer.Popup, listed above.DrawerPopupStatetypeimport type { DrawerPopupState } from '@nizivo/react/drawer'Name Type Default Description enteringrequiredboolean– exitingrequiredboolean– DrawerRootPropstypeimport type { DrawerRootProps } from '@nizivo/react/drawer'The props of
Drawer.Root, listed above.DrawerSidetypeimport type { DrawerSide } from '@nizivo/react/drawer'The edge the drawer slides in from, in logical terms:
startandendfollow the writing direction (endis the right edge in a left-to-right page, as Figma draws the Drawer);bottomis the sheet, as AppShell's "More" sheet below 640 px.type DrawerSide = "start" | "end" | "bottom"DrawerTitleLeveltypeimport type { DrawerTitleLevel } from '@nizivo/react/drawer'type DrawerTitleLevel = 2 | 3 | 4 | 5 | 6DrawerTitlePropstypeimport type { DrawerTitleProps } from '@nizivo/react/drawer'The props of
Drawer.Title, listed above.DrawerTriggerPropstypeimport type { DrawerTriggerProps } from '@nizivo/react/drawer'The props of
Drawer.Trigger, listed above.DrawerTriggerStatetypeimport type { DrawerTriggerState } from '@nizivo/react/drawer'The render state of
Drawer.Trigger, listed above.
Data attributes and slots
Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.
| Slot | Rendered by | Data attributes |
|---|---|---|
drawer-backdrop.nz-drawer-backdrop | Drawer.Popupinner element (CSS only) |
|
drawer-body.nz-drawer-body | Drawer.Bodyroot (takes render) | none |
drawer-close.nz-drawer-close | Drawer.Closeroot (takes render); interactive, focus: self |
|
drawer-content.nz-drawer-content | Drawer.Popupinner element (CSS only) | none |
drawer-description.nz-drawer-description | Drawer.Descriptionroot (takes render) | none |
drawer-footer.nz-drawer-footer | Drawer.Footerroot (takes render) | none |
drawer-header.nz-drawer-header | Drawer.Headerroot (takes render) | none |
drawer-popup.nz-drawer-popup | Drawer.Popuproot (no render); forced colours: border |
|
drawer-positioner.nz-drawer-positioner | Drawer.Popupinner element (CSS only) |
|
drawer-title.nz-drawer-title | Drawer.Titleroot (takes render) | none |
drawer-trigger.nz-drawer-trigger | Drawer.Triggerroot (takes render); interactive, focus: self |
|
icon-button-icon.nz-icon-button-icon | Drawer.Closeinner element (CSS only) | none |
CSS hooks
Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.
| Hook | Default | Read as |
|---|---|---|
--nz-drawer-bg | --nz-bg-surface |
|
--nz-drawer-radius | --nz-radius-xl |
|
--nz-drawer-width |
|
|
Layers: nz.components. Classes: .nz-drawer-backdrop, .nz-drawer-body, .nz-drawer-close, .nz-drawer-content, .nz-drawer-description, .nz-drawer-footer, .nz-drawer-header, .nz-drawer-popup, .nz-drawer-positioner, .nz-drawer-title, .nz-drawer-trigger, .nz-hit-target, .nz-icon-button.
Tokens this stylesheet reads (28)
--nz-bg-scrim, --nz-bg-surface, --nz-border-subtle, --nz-border-width-default, --nz-border-width-focus, --nz-duration-base, --nz-duration-fade, --nz-duration-slow, --nz-easing-enter, --nz-easing-exit, --nz-elevation-4, --nz-font-body-m, --nz-font-heading-h3, --nz-icon-secondary, --nz-radius-xl, --nz-size-control-md, --nz-size-icon-md, --nz-space-1, --nz-space-12, --nz-space-3, --nz-space-4, --nz-space-5, --nz-space-6, --nz-text-primary, --nz-text-secondary, --nz-tracking-body-m, --nz-tracking-heading-h3, --nz-z-overlay
Accessibility
These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:
drawer-close: draws the focus outline itself on keyboard focus.drawer-trigger: draws the focus outline itself on keyboard focus.
In forced-colours mode these states keep a visible system colour: drawer-popup (border).
Behaviour comes from the react-aria engine through Nizivo's adapter: focus management, ARIA wiring and keyboard handling are the engine's.
Pattern: WAI-ARIA Authoring Practices.
Keyboard
| Keys | Action |
|---|---|
| Enter, Space | On the trigger (a button), opens the drawer and moves focus to the first focusable element in its body, or to the drawer itself when the body has none. A control with autoFocus wins. |
| Tab, Shift+Tab | Moves focus between the focusable elements inside the drawer; focus stays inside while it is open. |
| Escape | Closes the drawer and returns focus to the trigger, or along the returnFocus chain below. |
Source: react-aria-components 1.22.1 ModalOverlay and Modal (react-aria 3.53.1 useModalOverlay, which contains focus), the Dialog's engine adapter (_engines/react-aria/drawer.client.tsx re-exports dialog.client.tsx). The first-focusable rule and focus return are Nizivo's (ARCHITECTURE.md §12.2), checked by the overlay lane (test/overlay/drawer.spec.tsx, nesting-popover-drawer.spec.tsx) in Chromium, Firefox and WebKit; apps/docs/scripts/smoke.ts checks opening with Enter, Tab staying inside, and Escape closing with focus back on the trigger.
Figma
Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.
- Canonical: none
- Ghost masters: Figma node
756:1001