A project intake form whose branch is chosen by a state machine, not hidden conditionals. The live path stays beside the form, the HUD reports the latency of your own interactions rather than a frame rate this form never earns, and the final brief contains only the answers for your chosen branch.
Preparing Brief Builder
Restoring your progress…
How this is built
Solid = NEXT, dashed = BACK, accent labels are guard names on the transition they decide.
The graph is generated from the shipped XState machine, and machine-wide events are listed once rather than drawn from every state. Initial state: projectType. Machine-wide events: SET_ANSWERS, RESET.
The flow is an explicit state machine. Branch guards, transition targets, and back-navigation live in machine data rather than reducer branches.
Why
The same definitions drive the running form and this diagram, so the documentation cannot quietly diverge from the behaviour.
Rejected
useReducer with a separately drawn diagram. The diagram would drift the first time a step moved, and nothing in the build would catch it.
Static guarded back-targets instead of a visited stack
Decision
Every BACK transition is declared in the machine and shares the same project-type guards as the forward branch.
Why
Back-navigation is part of the flow contract, so it belongs in the introspectable transition data and appears in the diagram.
Rejected
A visited-state array in context. XState v5 has no dynamic transition target, and a stack would be invisible to machine introspection.
Two state owners with one handoff
Decision
XState owns the step and direction; react-hook-form owns field state. A valid submit sends SET_ANSWERS and then NEXT.
Why
XState evaluates guards before transition actions, so a value-carrying NEXT would be judged against stale context. The two-event handoff commits values first.
Consequence
Progress is saved at step commit. Text typed since the last commit is not restored after a reload.
Rejected
Putting every field in machine context. That would re-render the machine per keystroke and duplicate the validation rules; debounced persistence would add writes and context churn without changing the useful outcome.
Direction-aware choreography with a reduced-motion alternative
Decision
Machine context carries direction. AnimatePresence uses it for a horizontal spring, while reduced motion substitutes a 120 ms crossfade and removes the error shake.
Why
The restored session already knows which way it was moving, and mode="wait" keeps one step in the DOM so heading focus is unambiguous.
Rejected
Animating layout or height. Text reflow makes those transitions jank-prone and can move a focus target during the transition.
Focus the step heading, not the first field
Decision
Each incoming step focuses its heading, while a persistent polite live region announces the step count and title.
Why
Screen-reader users receive the new context before being dropped into a field. The live region never remounts, so the announcement is reliable.
Rejected
Autofocusing the first control. It skips the heading and leaves the user hearing a field label without knowing which step it belongs to.