An ordered set of steps that collapse into editable summaries as they are completed, ending in one explicit commit.
A SetupFlow walks someone through an ordered set of dependent decisions that ends in one explicit commit. Exactly one step is open at a time; finished steps collapse to a one-line summary of what was chosen and stay editable; the last step reviews everything and holds the single action that commits it.
Reach for it when the decisions depend on each other and the whole configuration should be reviewable before it is committed. For a plain set of fields, use a Form with one submit. For a progress indicator across separate screens, use Stepper — see the wizard layout pattern. For sections that open in any order, use AccordionGroup.
Every step declares a value — a stable id, the same idea as a
Tabs tab's — plus its label and the summary it collapses to.
Everything the flow knows about a step is keyed by that value: which step is open, and
which ones are done. The footer moves the flow on.
A step is upcoming, active, complete, or invalidated, and that comes from the flow's own
state — there is no status prop. A status a feature can set by hand is a status that
drifts out of step with the flow it is describing.
Two things decide it, and both are answered by value:
value is the flow's currentStep.Nothing here is positional, so a step works out how it renders the first time it renders, without having to find out where it sits first.
The one thing the flow cannot work out for itself is invalidation: only the feature knows
that an edit to an earlier step broke a later one. Set isInvalidated for that, and name
what changed in invalidatedSummary rather than silently clearing the step. An
invalidated step has to be complete first — there is nothing to break in a step nobody
has answered.
Leave currentStep off and the flow tracks its own; pass it with onStepChange to
control it. Both are a step's value, so what onStepChange reports can be handed
straight back.
Name the step to open first in defaultCurrentStep. Without it the flow has no step to
open until its steps exist, and has to find the first of them once they do — one tick
during which nothing is open.
It is required when the steps themselves render asynchronously. The flow looks for its first step once, as it mounts, so steps that only arrive after a fetch resolves are not there to be found and the flow stays open at none. Naming the step settles it before any of them exist.
By default the flow keeps its own tally: moving off a step records it. That holds through an edit — reopening the first step of a finished flow adds to the tally rather than winding it back, so the steps after it stay complete.
Pass completedSteps when the feature knows better than the flow does: a flow resumed
from saved work, a step completed somewhere else, or one whose answers were thrown away.
It is the whole answer while it is set — the flow stops keeping its own.
isNextDisabled gates the step. Never let an invalid step advance — the review at the end
is a summary, not a second chance to catch what the step should have caught.
Mark the review step isFinal. It has no summary and no way to collapse, and its footer
holds the commit. Name the outcome in nextLabel — "Create export", "Send for previews" —
never "Finish" or "Done". A final step's footer has nothing to advance to, so give it an
onNext that commits.
On a completed step the whole header row is the control — a real <button> spanning the
step's columns, so the number, the title, the summary and the Edit affordance are all one
click target and one tab stop, and Enter and Space both activate it.
The Edit affordance at the trailing edge is a Link rendered as a span: it looks like a
link and does nothing on its own, so there is no control nested inside a control. The
row's accessible name is read off the row itself — the affordance, the title and the
summary, so "Edit Name the export Weekly orders · CSV", or "Review Name the export 2 of 4
fields are no longer available" while the step is invalidated. Set editLabel to change
the first part. A button's name replaces the text inside it, so the summary has to be part
of the name or a screen reader never hears the record of what was chosen.
Use onBeforeEdit to intercept, returning false to leave the step closed; the veto
covers the whole row, not just the affordance. Reopening an earlier step leaves later
steps complete — the flow never un-completes a step on its own. Mark one isInvalidated
when the change genuinely broke it, or drop it from completedSteps when its answers are
gone.
isDisabled marks a step the flow cannot open. It keeps its place in the numbering, but
the flow steps over it on the way to the next one — and the same walk decides which step
an unseeded flow starts at, so a disabled first step is not where the flow opens. It
renders no control to reopen it either.
Naming a disabled step in currentStep or defaultCurrentStep still opens it. That is
the feature overruling the flow, and the flow takes the feature's word for it.
Only the step's marker recedes into the disabled palette; the title and summary stay at
full strength. Say why the step is unavailable in its summary rather than leaving the
grey marker to carry it.
Advancing the flow, or reopening an earlier step, moves focus to the newly opened step's title so a keyboard or screen reader user lands on the step they now have to fill in. A flow that has only just rendered never takes focus, including one that finds its first step a tick after mount.
The container header and the close guard belong to whatever holds the flow. A flow with unsaved input should guard closing with a ConfirmationModal; a pristine one closes silently. For a flow beside live preview content, see Setup flow with a preview.
PlumaSetupFlow extends BoxThe value of every step to count as complete. Leave it off and the flow keeps its
own tally: moving off a step records it. Set it when the feature knows better than
the flow does — a step completed elsewhere, or one whose answers were thrown away.
The value of the step that is currently open. Pass it together with
onStepChange to control the flow; leave it off to let the flow track its own.
The value of the step open on first render. Ignored when currentStep is set.
Prefer setting it. With nothing named, the flow has no step to open until its steps exist, and has to find the first of them once they do — one tick during which no step is open.
Required when the steps themselves render asynchronously. The flow looks for its first step once, as it mounts; steps that arrive after a fetch has resolved are not there to be found, and the flow stays open at none. Naming the step settles it before any of them exist, which is the only answer that doesn't make a step's arrival re-open the flow underneath the customer.
Called when the flow moves to another step, with that step's value.
PlumaSetupFlowStep extends BoxLabel for the control that reopens a completed step.
Replaces summary while the step is invalidated. Name what changed — "2 of 4
fields are no longer available" — rather than only that something did.
Whether the step is unavailable. A disabled step cannot be opened or edited: the flow steps over it on its way to the next one, and it offers no control to reopen it. Its marker recedes into the disabled palette; its words stay readable.
Naming it in currentStep or defaultCurrentStep still opens it — that is the
feature overruling the flow, and the flow takes the feature's word for it.
Marks the step that reviews every earlier decision and holds the flow's single commit action. A final step has no summary and cannot be collapsed.
Whether an edit to an earlier step has broken this step's values. Set it from the feature — the flow can't know what invalidates what. An invalidated step shows a caution marker and must be re-confirmed before the flow can be committed.
The step's title. Keep it a short verb phrase — "Choose data", not "Data".
Called before a completed step reopens. Return false to leave it closed.
A one-line record of what was chosen, shown once the step is complete. Give the values themselves — "Weekly orders · CSV" — not a description of the step.
A stable identifier for the step, unique within the flow. Everything the flow knows about a step is keyed by it: which step is open, and which are done.