Marble UI
← Components

Dialog Stack

Preview: Default

Multi-step flow in one modal where earlier steps stay visible as cards stacked behind the active one. Back/Next move through steps; the title of each step is the dialog's accessible name. Steps stay mounted while the dialog is open, so uncontrolled fields keep their values on Back/Next; they reset when it closes.

Import

import { DialogStack } from '@marbleui/ui'

Props

<DialogStack>

PropTypeDefaultDescription
steps *requiredStackStep[]—
trigger *requiredReactNode—
finishLabelstring'Done'
onFinish() => void—

Accessibility

Keyboard interaction, focus management and ARIA semantics come from the underlying primitives: React Aria Dialog · React Aria DialogTrigger · React Aria Heading · React Aria Modal · React Aria ModalOverlay. Follow the primitive's docs for the full keyboard map.

Usage notes

Multi-step flow in one modal; completed steps stay visible as cards stacked behind the current one. For a step indicator on a page use Stepper; for a single form use Dialog.

<DialogStack trigger={<Button>Create workspace</Button>} finishLabel="Create" onFinish={() => createWorkspace({ name, plan })} steps={[
  { id: 'name', title: 'Name your workspace', content: <TextField label="Workspace name" value={name} onChange={setName} /> },
  { id: 'plan', title: 'Choose a plan', content: <RadioGroup aria-label="Plan" value={plan} onChange={setPlan}>…</RadioGroup> },
]} />
  • Every step stays mounted (inactive ones hidden) while the dialog is open, so uncontrolled fields keep their values on Back and Next. Closing the dialog unmounts them; keep values you need in your own state, as above.
  • There’s no validation hook: Next always advances, and onFinish gets no arguments.
  • Cancel (on step 1), Escape or clicking outside closes the dialog and resets it to step 1.
  • The current step’s title is the dialog’s accessible name; the stacked cards behind are hidden from screen readers.
Example source · show
import { Button } from '../button'
import { Radio, RadioGroup } from '../radio-group'
import { TextField } from '../text-field'
import { DialogStack } from './dialog-stack'

export const styles = {
  Default: () => (
    <DialogStack trigger={<Button>Create workspace</Button>} finishLabel="Create" steps={[
      { id: 'name', title: 'Name your workspace', content: <TextField label="Workspace name" defaultValue="Acme" autoFocus /> },
      { id: 'plan', title: 'Choose a plan', content: <RadioGroup aria-label="Plan" defaultValue="team"><Radio value="free" variant="card" description="Up to 3 members">Free</Radio><Radio value="team" variant="card" description="$24 per seat">Team</Radio></RadioGroup> },
      { id: 'invite', title: 'Invite your team', content: <TextField label="Emails" placeholder="ada@acme.com, grace@acme.com" /> },
    ]} />
  ),
}

Source: packages/ui/src/dialog-stack/dialog-stack.tsx

Catalog

Category
overlays
Priority
Extra
Primitive
composed
React Aria
Modal + Dialog
Base UI
Dialog