Dual Pane
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.
Properties
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 |