Drawer
The Drawer component provides a way to display content in a panel that slides in from the edge of the screen. Supports multiple configuration options, including direction (left, right, top, bottom), size constraints, and nested drawers for building complex, multi-level interfaces.
It is composed of several subcomponents such as Drawer.Root, Drawer.Trigger, Drawer.Content, Drawer.Header, Drawer.HeaderV2, Drawer.Tagline, Drawer.Title, Drawer.Description, Drawer.Body, Drawer.Footer, Drawer.DualPane, Drawer.Rail, Drawer.Steps, Drawer.Step, Drawer.SubStep, Drawer.DualPaneMain and Drawer.Close to offer a structured and customizable interface.
Usage
import { Drawer } from '@harnessio/ui/components'
//...
return ( <Drawer.Root direction="left" open={isDrawerOpen} onOpenChange={setIsDrawerOpen} > <Drawer.Trigger> <Button>Open drawer</Button> </Drawer.Trigger>
<Drawer.Content size="md"> <Drawer.Header icon="info-circle-solid" hideClose> <Drawer.Tagline>Section</Drawer.Tagline> <Drawer.Title>Title of the drawer</Drawer.Title> <Drawer.Description>Description of the drawer</Drawer.Description> </Drawer.Header>
<Drawer.Body className=""> Content of the drawer goes here
<Drawer.Root> <Drawer.Trigger> <Button>Open Nested Drawer</Button> </Drawer.Trigger>
<Drawer.Content> <Drawer.Header> <Drawer.Title>Title</Drawer.Title> </Drawer.Header>
<Drawer.Body> Content </Drawer.Body>
<Drawer.Footer> Footer </Drawer.Footer> </Drawer.Content> </Drawer.Root> </Drawer.Body>
<Drawer.Footer> <ButtonLayout.Root> <ButtonLayout.Primary> <Button>Action</Button> </ButtonLayout.Primary> <ButtonLayout.Secondary> <Drawer.Close asChild> <Button variant="outline">Cancel</Button> </Drawer.Close> </ButtonLayout.Secondary> </ButtonLayout.Root> </Drawer.Footer> </Drawer.Content> </Drawer.Root>)Anatomy
All parts of the Drawer component can be imported and composed as required.
<Drawer.Root> <Drawer.Trigger /> <Drawer.Content> <Drawer.Header> <Drawer.Tagline /> <Drawer.Title /> <Drawer.Description /> </Drawer.Header> <Drawer.Body /> <Drawer.Footer> <Drawer.Close /> </Drawer.Footer> </Drawer.Content></Drawer.Root>Nested drawers
Nested drawers can be implemented by placing a Drawer.Root inside the parent Drawer.Content.
When a wider drawer opens over a narrower one, the narrower drawer automatically expands to peek out from behind, preserving the visual stacking effect.
Max stack depth
The Drawer.Root component accepts a maxStackDepth prop (default 7) that controls how many nested drawers visually stack. Drawers beyond this limit still open normally but cover the deepest visible drawer instead of adding more visual offset.
Drawer sizes
The Drawer.Content component accepts a size prop to control its width (or height, depending on direction). Supported values are: 2xs, xs, sm, md, lg, xl, and full.
Drawer opening direction
The Drawer.Root component accepts a direction prop to control which side the drawer opens from. Supported values are: right, left, top, and bottom.
Keep focus on the custom trigger element
If a drawer is opened without the Trigger component (e.g., a dropdown item), focus will not return to the trigger element when the drawer is closed. To address this use useCustomDialogTrigger hook to register your custom trigger element.
HeaderV2
Drawer.HeaderV2 is a structured, opinionated header that consolidates icon/logo, title, description, actions, metadata (children), and tabs into a single component. It replaces the compositional pattern of Drawer.Header > Drawer.Title > Drawer.Description when you want a standardized layout with optional loading state.
Basic
With logo, actions, and metadata
With tabs
Wrap the drawer content in a Tabs.Root to enable tab switching. Drawer.HeaderV2 renders the tab strip at the bottom of the header when tabs is provided.
Loading state
When isLoading is true, actions are hidden and the children (metadata) slot renders a skeleton placeholder.
HeaderV2
Prop | Required | Default | Type |
|---|---|---|---|
| title | true | string | |
| description | false | undefined | string |
| icon | false | undefined | IconV2NamesType | { logo: LogoV2NamesType } |
| actions | false | undefined | ReactNode |
| tabs | false | undefined | HeaderV2TabItem[] |
| hideClose | false | false | boolean |
| isLoading | false | false | boolean |
| children | false | undefined | ReactNode |
| className | false | undefined | string |
Dual pane drawer
The dual pane drawer renders a fixed left rail beside a scrollable main pane. Use it whenever content benefits from a side-by-side layout, for example:
- Multi-step flows — a numbered step list in the rail and the active step’s form in the main pane.
- Reference while you work — a glossary, definition list, file tree, related records, or other supporting content in the rail that the user can scan while completing a form, editor, or other primary task in the main pane.
- Browse and detail — a list of items in the rail with the selected item’s detail view in the main pane.
The rail and the main pane each have their own scroll area, so users keep context on one side while moving through the other.
For non-stepped content (reference, browse, anything where the rail isn’t a wizard), use Drawer.Rail. It provides the rail’s chrome — fixed width, surface, border, optional title header, and an independent scroll area — and accepts arbitrary children, so you can drop in a definition list, file tree, navigation menu, or any other content.
For multi-step flows, compose the rail with Drawer.Steps and Drawer.Step — a numbered list with active / completed / upcoming states. Each step renders an indicator circle (check mark for completed, number for active or upcoming) plus a title and optional description. Put the active step content in Drawer.DualPaneMain with a separate scrollable Drawer.Body. Control the active step with Drawer.Steps’ value and onValueChange. Any step the user has previously visited stays clickable — including steps ahead of the current one if they’ve already been reached — so the user can jump back and forth freely. Only steps the user has never visited are read-only.
For sub-flows inside a step, nest Drawer.SubStep children inside a Drawer.Step. Substeps appear under their parent step when the parent is active, with simpler indicators (check mark for completed, dot for active, dash for upcoming) and the same navigation rules: any visited substep stays clickable. Setting Drawer.Steps value to a substep’s value automatically marks the parent step as active.
Use size="md", size="lg", or size="xl" on Drawer.Content so the rail and the main pane both have room. The panes never stack — the right pane stays beside the rail and shrinks with the viewport down to the xs drawer width as a minimum; past that point the dual pane scrolls horizontally. This applies at every size (including the fixed sizes), because Drawer.Content is also capped at the viewport’s width.
<Drawer.Root open={open} onOpenChange={setOpen}> <Drawer.Trigger> <Button>Open dual pane drawer</Button> </Drawer.Trigger>
<Drawer.Content size="lg"> <Drawer.DualPane> <Drawer.Steps value={currentStep} onValueChange={setCurrentStep} title="Title"> <Drawer.Step value="details" title="Step Title" description="Description" /> <Drawer.Step value="configuration" title="Step Title" description="Description" /> <Drawer.Step value="permissions" title="Step Title" description="Description" /> <Drawer.Step value="review" title="Step Title" description="Description" /> </Drawer.Steps>
<Drawer.DualPaneMain> <Drawer.Header> <Drawer.Tagline>{currentIndex + 1} out of {steps.length} steps</Drawer.Tagline> <Drawer.Title>{activeStep.title}</Drawer.Title> <Drawer.Description>{activeStep.description}</Drawer.Description> </Drawer.Header>
<Drawer.Body> <div className="flex h-full items-center justify-center"> <p className="font-heading-section text-cn-3">Content</p> </div> </Drawer.Body>
<Drawer.Footer> <ButtonLayout.Root> <ButtonLayout.Primary> <Button onClick={goNext}>Continue</Button> </ButtonLayout.Primary> <ButtonLayout.Secondary> <Drawer.Close asChild> <Button variant="outline">Cancel</Button> </Drawer.Close> </ButtonLayout.Secondary> </ButtonLayout.Root> </Drawer.Footer> </Drawer.DualPaneMain> </Drawer.DualPane> </Drawer.Content></Drawer.Root>Substeps
Nest Drawer.SubStep children inside a Drawer.Step to give that step its own sub-flow. Substeps appear under their parent when the parent is the active step (driven by setting Drawer.Steps’ value to the parent’s value or to any of its substep values). They use simpler indicators — check mark for completed, filled dot for active, dash for upcoming — and follow the same navigation rules as top-level steps: any substep the user has previously visited stays clickable so they can move freely between them, while substeps that have never been reached are read-only.
const [currentStep, setCurrentStep] = useState('step2.2');
<Drawer.Steps value={currentStep} onValueChange={setCurrentStep} title="Title"> <Drawer.Step value="step1" title="Step 1" description="Description" />
{/* Drawer.Step accepts Drawer.SubStep children for sub-flows. */} <Drawer.Step value="step2" title="Step 2" description="Description"> <Drawer.SubStep value="step2.1" title="Step 2.1" /> <Drawer.SubStep value="step2.2" title="Step 2.2" /> <Drawer.SubStep value="step2.3" title="Step 2.3" /> </Drawer.Step>
<Drawer.Step value="step3" title="Step 3" description="Description" /></Drawer.Steps>Reference content
Use Drawer.Rail when the rail holds reference material that the user scans while they work in the main pane — a glossary, a definition list, a related-records list, a file tree, etc. The rail keeps the chrome (surface, border, optional title, independent scroll area) but doesn’t enforce any list semantics, so you can render any markup you like inside.
<Drawer.Root open={open} onOpenChange={setOpen}> <Drawer.Trigger> <Button>Open dual pane drawer</Button> </Drawer.Trigger>
<Drawer.Content size="lg"> <Drawer.DualPane> <Drawer.Rail title="Rail" aria-label="Rail"> <div className="flex h-full items-center justify-center"> <p className="font-heading-section text-cn-3">Content</p> </div> </Drawer.Rail>
<Drawer.DualPaneMain> <Drawer.Header> <Drawer.Title>Title</Drawer.Title> <Drawer.Description>Description</Drawer.Description> </Drawer.Header>
<Drawer.Body> <div className="flex h-full items-center justify-center"> <p className="font-heading-section text-cn-3">Content</p> </div> </Drawer.Body>
<Drawer.Footer> <ButtonLayout.Root> <ButtonLayout.Primary> <Button>Create pipeline</Button> </ButtonLayout.Primary> <ButtonLayout.Secondary> <Drawer.Close asChild> <Button variant="outline">Cancel</Button> </Drawer.Close> </ButtonLayout.Secondary> </ButtonLayout.Root> </Drawer.Footer> </Drawer.DualPaneMain> </Drawer.DualPane> </Drawer.Content></Drawer.Root>Nested drawer
A dual pane drawer can host nested drawers the same way any other drawer can — drop a Drawer.Root anywhere inside the parent’s content (typically inside Drawer.DualPaneMain, but the rail works too) and it automatically becomes a nested drawer. Use this when an action inside the dual pane needs its own focused workspace — for example creating a sub-resource referenced by the parent form, viewing a record’s full detail, or confirming a destructive change — without losing the user’s place in the parent flow.
While the nested drawer is open the parent dual pane stays mounted (its state, scroll position, and active step are preserved) and shifts back slightly to give the nested drawer visual prominence. Closing the nested drawer restores the parent.
<Drawer.Root open={open} onOpenChange={setOpen}> <Drawer.Trigger> <Button>Open dual pane drawer with nested drawer</Button> </Drawer.Trigger>
<Drawer.Content size="lg"> <Drawer.DualPane> <Drawer.Steps title="Title" value={step} onValueChange={setStep}> <Drawer.Step value="details" title="Step" description="Description" /> <Drawer.Step value="trigger" title="Step" description="Description" /> <Drawer.Step value="review" title="Step" description="Description" /> </Drawer.Steps>
<Drawer.DualPaneMain> <Drawer.Header> <Drawer.Title>Step Title</Drawer.Title> <Drawer.Description>Description</Drawer.Description> </Drawer.Header>
<Drawer.Body> {/* Drawer.Root placed inside the parent's content automatically renders as a nested drawer (no `nested` prop required). */} <Drawer.Root> <Drawer.Trigger> <Button variant="outline">Open another drawer</Button> </Drawer.Trigger>
<Drawer.Content size="sm"> <Drawer.Header> <Drawer.Title>Title</Drawer.Title> <Drawer.Description>Description</Drawer.Description> </Drawer.Header> <Drawer.Body> <div className="flex h-full items-center justify-center"> <p className="font-heading-section text-cn-3">Content</p> </div> </Drawer.Body> <Drawer.Footer> <ButtonLayout.Root> <ButtonLayout.Primary> <Drawer.Close asChild> <Button>Create connector</Button> </Drawer.Close> </ButtonLayout.Primary> <ButtonLayout.Secondary> <Drawer.Close asChild> <Button variant="outline">Cancel</Button> </Drawer.Close> </ButtonLayout.Secondary> </ButtonLayout.Root> </Drawer.Footer> </Drawer.Content> </Drawer.Root> </Drawer.Body>
<Drawer.Footer>{/* parent footer */}</Drawer.Footer> </Drawer.DualPaneMain> </Drawer.DualPane> </Drawer.Content></Drawer.Root>Sizes
The dual pane drawer inherits its width from Drawer.Content’s size prop. The panes always render side by side. The right pane is fluid — it shrinks with the available space — but it has a min-width of the xs drawer size (var(--cn-drawer-xs)) so it never collapses below a usable width. If the drawer becomes narrower than the steps rail plus that minimum (for example a size="full" drawer on a narrow viewport, a fixed size like lg on a viewport that’s smaller than lg, or a small fixed size), the dual pane scrolls horizontally instead of clipping content. Use md or larger to give both panes comfortable room without scrolling.
Drawer.Content itself is capped at the viewport’s width and height (100vw / 100vh), so a fixed size never overflows the screen — when the viewport is narrower than the chosen size, the drawer fills the viewport and the dual pane behaves the same as full.
You can override the right pane’s floor by setting --cn-drawer-dual-pane-main-min-width on .cn-drawer-dual-pane.
DualPane
Dual-pane row layout wrapper.
Prop | Required | Default | Type |
|---|---|---|---|
| children | true | ReactNode | |
| className | false | undefined | string |
Rail
Generic left rail for a dual pane drawer. Provides the rail’s chrome — fixed width, surface, border, optional title header, and an independent scroll area — and renders arbitrary children. Use this for non-stepped content like a glossary, definition list, file tree, related-records list, or browse view. For numbered multi-step flows, use Drawer.Steps instead.
Renders an <aside> element so screen readers expose it as a complementary region; pair it with aria-label to give that region a name.
Prop | Required | Default | Type |
|---|---|---|---|
| title | false | undefined | ReactNode |
| aria-label | false | undefined | string |
| className | false | undefined | string |
| children | true | ReactNode |
Steps
Left step rail with independent scrolling and controlled active step state.
Prop | Required | Default | Type |
|---|---|---|---|
| value | true | string | |
| onValueChange | false | undefined | (value: string) => void |
| title | false | undefined | ReactNode |
| aria-label | false | Drawer steps | string |
| children | true | ReactNode |
Step
A single step in the step rail. Renders an indicator circle with the step number for active and upcoming steps, and a check mark for completed steps. Active and completed steps are rendered as buttons; upcoming steps are read-only. Pass Drawer.SubStep children to give the step its own sub-flow — substeps appear under the step when it is active.
Prop | Required | Default | Type |
|---|---|---|---|
| value | true | string | |
| title | true | ReactNode | |
| description | false | undefined | ReactNode |
| children | false | undefined | ReactNode |
SubStep
A single substep nested inside a Drawer.Step. Renders a smaller, no-circle indicator (check mark for completed, filled dot for active, dash for upcoming) plus a title. Completed and active substeps are rendered as buttons; upcoming substeps are read-only. The parent step is automatically treated as active whenever any of its substeps is the rail’s value, and top-level steps after the parent stay upcoming until the user moves past the parent.
Prop | Required | Default | Type |
|---|---|---|---|
| value | true | string | |
| title | true | ReactNode |
DualPaneMain
Right-hand column for the active step header, body, and footer.
Prop | Required | Default | Type |
|---|---|---|---|
| children | true | ReactNode | |
| className | false | undefined | string |
API Reference
Root
Contains Drawer.Trigger and Drawer.Content components.
Prop | Required | Default | Type |
|---|---|---|---|
| defaultOpen | false | undefined | boolean |
| open | false | undefined | boolean |
| onOpenChange | false | undefined | (open: boolean) => void |
| modal | false | true | boolean |
| container | false | document.body | HTMLElement |
| direction | false | right | 'right' | 'left' | 'top' | 'bottom' |
| onAnimationEnd | false | undefined | (open: boolean) => void |
| dismissible | false | true | boolean |
| handleOnly | false | false | boolean |
| repositionInputs | false | true | boolean |
| maxStackDepth | false | 7 | number |
| children | true | ReactNode |
Trigger
Used to open a drawer. Essential for registering the trigger element with the dialog focus manager.
If a drawer is opened without the Trigger component (e.g., a dropdown item), focus will not return to the trigger element when the drawer is closed. To address this use useCustomDialogTrigger hook to register your custom trigger element.
Prop | Required | Default | Type |
|---|---|---|---|
| children | true | ReactNode |
Close
An optional button that closes the drawer.
Prop | Required | Default | Type |
|---|---|---|---|
| asChild | false | false | boolean |
Content
Contains Drawer.Header, Drawer.Body and Drawer.Footer components to be rendered in the open drawer.
Prop | Required | Default | Type |
|---|---|---|---|
| size | false | sm | '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full' |
| children | true | ReactNode | |
| overlayClassName | false | undefined | string |
| forceWithOverlay | false | false | boolean |
Header
An optional container for the Drawer.Tagline, Drawer.Title and Drawer.Description components.
Prop | Required | Default | Type |
|---|---|---|---|
| icon | false | undefined | string |
| hideClose | false | false | boolean |
| logo | false | undefined | string |
| children | true | ReactNode |
Tagline
An optional tagline above the title.
Prop | Required | Default | Type |
|---|---|---|---|
| children | true | ReactNode |
Title
An optional accessible title to be announced when the drawer is opened.
Prop | Required | Default | Type |
|---|---|---|---|
| asChild | false | false | boolean |
Description
An optional accessible description to be announced when the drawer is opened.
Prop | Required | Default | Type |
|---|---|---|---|
| asChild | false | false | boolean |
Body
A scrollable wrapper for all content that doesn’t belong to the Drawer.Header or Drawer.Footer.
Prop | Required | Default | Type |
|---|---|---|---|
| className | false | undefined | string |
| scrollAreaClassName | false | undefined | string |
| children | true | ReactNode |
Footer
An optional footer wrapper.