Skip to content
Nizivo UI

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.

Client componentEngine: react-aria

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

It opens on page load (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.

NameTypeDefaultDescription
childrenrequiredReactNode–
defaultOpenboolean–
onOpenChange(open: boolean, details: ChangeDetails<OpenReason>) => void–

details.reason is 'unknown' with React Aria, which reports no cause.

openboolean–
returnFocusReturnFocusTargettrue

Where focus goes when the drawer closes (§12.2 Focus return), as Dialog.Root's: the trigger, else the trigger of the overlay it was opened from, else <body> with a development warning; an element ref or a function returning one goes first; false leaves focus alone.

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.

NameTypeDefaultDescription
childrenReactNode–
renderRenderProp<DrawerTriggerState>–

Must render a <button>, for example render={<Button>View order</Button>}.

  • Also takes every attribute of <button> except children. (Omit<ComponentPropsWithRef<"button">, "children">)

render takes an element, or, in client code, a function of the props and this state (DrawerTriggerState):

NameTypeDefaultDescription
disabledrequiredboolean–
focusVisiblerequiredboolean–
openrequiredboolean–
pressedrequiredboolean–

Drawer.Popup

Client component Renders data-slot="drawer-popup" with the class nz-drawer-popup. Props: DrawerPopupProps. Source: src/drawer/drawer.client.tsx.

NameTypeDefaultDescription
backdropboolean–

The scrim element. Defaults to on for the root modal and off when opened from another modal.

childrenrequiredReactNode–
containerElement | null–

Portal container. Defaults to the parent overlay, else document.body (§12.1 L2).

sideDrawerSide
"start""end""bottom"
'end'

The edge it slides in from.

  • Also takes every attribute of <div> except children, 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.

NameTypeDefaultDescription
renderRenderProp<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.

NameTypeDefaultDescription
levelDrawerTitleLevel
23456
2
renderRenderProp<Record<string, never>>–
  • Also takes every attribute of <h2> except id. (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.

NameTypeDefaultDescription
renderRenderProp<Record<string, never>>–
  • Also takes every attribute of <p> except id. (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.

NameTypeDefaultDescription
renderRenderProp<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.

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.

NameTypeDefaultDescription
renderRenderProp<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.

NameTypeDefaultDescription
childrenReactNode–

Text content, rendered in a plain <button>; leave it out for the default IconButton.

labelsPartial<Pick<DrawerLabels, "close">>–

The default IconButton's accessible name; an aria-label you pass wins.

renderRenderProp<DrawerCloseState>–

Must render a <button>, for example render={<Button>Cancel</Button>}. With neither render nor children, the part is an IconButton showing the close glyph, named by labels.close, which sits in the header's end corner when it is a direct child of Drawer.Popup.

  • Also takes every attribute of <button> except children. (Omit<ComponentPropsWithRef<"button">, "children">)

render takes an element, or, in client code, a function of the props and this state (DrawerCloseState):

NameTypeDefaultDescription
disabledrequiredboolean–
focusVisiblerequiredboolean–
pressedrequiredboolean–

Other exports

DrawerBodyProps type

import type { DrawerBodyProps } from '@nizivo/react/drawer'

The props of Drawer.Body, listed above.

DrawerCloseProps type

import type { DrawerCloseProps } from '@nizivo/react/drawer'

The props of Drawer.Close, listed above.

DrawerCloseState type

import type { DrawerCloseState } from '@nizivo/react/drawer'

The render state of Drawer.Close, listed above.

DrawerDescriptionProps type

import type { DrawerDescriptionProps } from '@nizivo/react/drawer'

The props of Drawer.Description, listed above.

DrawerFooterProps type

import type { DrawerFooterProps } from '@nizivo/react/drawer'

The props of Drawer.Footer, listed above.

DrawerHeaderProps type

import type { DrawerHeaderProps } from '@nizivo/react/drawer'

The props of Drawer.Header, listed above.

DrawerLabels type

import type { DrawerLabels } from '@nizivo/react/drawer'

Strings the Drawer renders (§9.3 rule 7, §16.4). close is the accessible name of the default Drawer.Close (an IconButton showing the close glyph), set through that part's labels. Engine-owned with React Aria (not overridable): dismiss, the visually hidden "Dismiss" button rendered inside the drawer.

NameTypeDefaultDescription
closerequiredstring'Close'
dismissrequiredstring–
DrawerPopupProps type

import type { DrawerPopupProps } from '@nizivo/react/drawer'

The props of Drawer.Popup, listed above.

DrawerPopupState type

import type { DrawerPopupState } from '@nizivo/react/drawer'

NameTypeDefaultDescription
enteringrequiredboolean–
exitingrequiredboolean–
DrawerRootProps type

import type { DrawerRootProps } from '@nizivo/react/drawer'

The props of Drawer.Root, listed above.

DrawerSide type

import type { DrawerSide } from '@nizivo/react/drawer'

The edge the drawer slides in from, in logical terms: start and end follow the writing direction (end is the right edge in a left-to-right page, as Figma draws the Drawer); bottom is the sheet, as AppShell's "More" sheet below 640 px.

type DrawerSide = "start" | "end" | "bottom"

DrawerTitleLevel type

import type { DrawerTitleLevel } from '@nizivo/react/drawer'

type DrawerTitleLevel = 2 | 3 | 4 | 5 | 6

DrawerTitleProps type

import type { DrawerTitleProps } from '@nizivo/react/drawer'

The props of Drawer.Title, listed above.

DrawerTriggerProps type

import type { DrawerTriggerProps } from '@nizivo/react/drawer'

The props of Drawer.Trigger, listed above.

DrawerTriggerState type

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

SlotRendered byData attributes
drawer-backdrop
.nz-drawer-backdrop
Drawer.Popup
inner element (CSS only)
  • data-entering (present or absent)
    Enter transition running; the node stays mounted until it ends
  • data-exiting (present or absent)
    Exit transition running; the node stays mounted until it ends
  • data-open (present or absent)
    Open
drawer-body
.nz-drawer-body
Drawer.Body
root (takes render)
none
drawer-close
.nz-drawer-close
Drawer.Close
root (takes render); interactive, focus: self
  • data-disabled (present or absent) ← disabled
    Mirrors the disabled attribute
  • data-focus-visible (present or absent)
    Focus is visible; elsewhere use :focus-visible
  • data-pressed (present or absent)
    Pointer or keyboard press (native parts use :active)
drawer-content
.nz-drawer-content
Drawer.Popup
inner element (CSS only)
none
drawer-description
.nz-drawer-description
Drawer.Description
root (takes render)
none
drawer-footer
.nz-drawer-footer
Drawer.Footer
root (takes render)
none
drawer-header
.nz-drawer-header
Drawer.Header
root (takes render)
none
drawer-popup
.nz-drawer-popup
Drawer.Popup
root (no render); forced colours: border
  • data-entering (present or absent)
    Enter transition running; the node stays mounted until it ends
  • data-exiting (present or absent)
    Exit transition running; the node stays mounted until it ends
  • data-nested-open (present or absent)
    A child modal is open
  • data-open (present or absent)
    Open
  • data-side: "top""right""bottom""left"
    Resolved physical side after flip and RTL; the side prop takes logical values
drawer-positioner
.nz-drawer-positioner
Drawer.Popup
inner element (CSS only)
  • data-entering (present or absent)
    Enter transition running; the node stays mounted until it ends
  • data-exiting (present or absent)
    Exit transition running; the node stays mounted until it ends
  • data-open (present or absent)
    Open
  • data-side: "top""right""bottom""left"
    Resolved physical side after flip and RTL; the side prop takes logical values
drawer-title
.nz-drawer-title
Drawer.Title
root (takes render)
none
drawer-trigger
.nz-drawer-trigger
Drawer.Trigger
root (takes render); interactive, focus: self
  • data-disabled (present or absent) ← disabled
    Mirrors the disabled attribute
  • data-focus-visible (present or absent)
    Focus is visible; elsewhere use :focus-visible
  • data-open (present or absent)
    Open
  • data-pressed (present or absent)
    Pointer or keyboard press (native parts use :active)
icon-button-icon
.nz-icon-button-icon
Drawer.Close
inner 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.

HookDefaultRead as
--nz-drawer-bg--nz-bg-surface
  • background on .nz-drawer-popup
--nz-drawer-radius--nz-radius-xl
  • --_nz-drawer-radius on .nz-drawer-popup
--nz-drawer-width
  • default: 27.5rem
  • inline-size on .nz-drawer-popup:is([data-side="left"], [data-side="right"])

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

KeysAction
Enter, SpaceOn 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+TabMoves focus between the focusable elements inside the drawer; focus stays inside while it is open.
EscapeCloses 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.

On this page