Skip to content

Drawer

beta

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
titletruestring
descriptionfalseundefinedstring
iconfalseundefinedIconV2NamesType | { logo: LogoV2NamesType }
actionsfalseundefinedReactNode
tabsfalseundefinedHeaderV2TabItem[]
hideClosefalsefalseboolean
isLoadingfalsefalseboolean
childrenfalseundefinedReactNode
classNamefalseundefinedstring

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.Stepsvalue 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.Stepsvalue 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
childrentrueReactNode
classNamefalseundefinedstring

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
titlefalseundefinedReactNode
aria-labelfalseundefinedstring
classNamefalseundefinedstring
childrentrueReactNode

Steps

Left step rail with independent scrolling and controlled active step state.

Prop
Required
Default
Type
valuetruestring
onValueChangefalseundefined(value: string) => void
titlefalseundefinedReactNode
aria-labelfalseDrawer stepsstring
childrentrueReactNode

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
valuetruestring
titletrueReactNode
descriptionfalseundefinedReactNode
childrenfalseundefinedReactNode

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
valuetruestring
titletrueReactNode

DualPaneMain

Right-hand column for the active step header, body, and footer.

Prop
Required
Default
Type
childrentrueReactNode
classNamefalseundefinedstring

API Reference

Root

Contains Drawer.Trigger and Drawer.Content components.

Prop
Required
Default
Type
defaultOpenfalseundefinedboolean
openfalseundefinedboolean
onOpenChangefalseundefined(open: boolean) => void
modalfalsetrueboolean
containerfalsedocument.bodyHTMLElement
directionfalseright'right' | 'left' | 'top' | 'bottom'
onAnimationEndfalseundefined(open: boolean) => void
dismissiblefalsetrueboolean
handleOnlyfalsefalseboolean
repositionInputsfalsetrueboolean
maxStackDepthfalse7number
childrentrueReactNode

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
childrentrueReactNode

Close

An optional button that closes the drawer.

Prop
Required
Default
Type
asChildfalsefalseboolean

Content

Contains Drawer.Header, Drawer.Body and Drawer.Footer components to be rendered in the open drawer.

Prop
Required
Default
Type
sizefalsesm '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full'
childrentrueReactNode
overlayClassNamefalseundefinedstring
forceWithOverlayfalsefalseboolean

An optional container for the Drawer.Tagline, Drawer.Title and Drawer.Description components.

Prop
Required
Default
Type
iconfalseundefinedstring
hideClosefalsefalseboolean
logofalseundefinedstring
childrentrueReactNode

Tagline

An optional tagline above the title.

Prop
Required
Default
Type
childrentrueReactNode

Title

An optional accessible title to be announced when the drawer is opened.

Prop
Required
Default
Type
asChildfalsefalseboolean

Description

An optional accessible description to be announced when the drawer is opened.

Prop
Required
Default
Type
asChildfalsefalseboolean

Body

A scrollable wrapper for all content that doesn’t belong to the Drawer.Header or Drawer.Footer.

Prop
Required
Default
Type
classNamefalseundefinedstring
scrollAreaClassNamefalseundefinedstring
childrentrueReactNode

An optional footer wrapper.