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>
| Prop | Type | Default | Description |
|---|---|---|---|
| steps *required | StackStep[] | — | |
| trigger *required | ReactNode | — | |
| finishLabel | string | '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
onFinishgets no arguments. - Cancel (on step 1), Escape or clicking outside closes the dialog and resets it to step 1.
- The current step’s
titleis the dialog’s accessible name; the stacked cards behind are hidden from screen readers.
Example source · show· hide
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