A review step is where users catch their own mistakes, but most implementations undermine it: the “Change” link sends the user back into the wizard and makes them click Next through every later step to return, and answers made stale by the change are submitted anyway.

In a multi-step form state machine the review step is a state like any other, with two special jobs. It must render the committed answers in a form people can scan, and it must be a hub: every answer links to its step, and finishing that step returns straight to review — after revalidating anything the edit affected.


Context and prerequisites

A good review step does four things:

  • Summarises answers as the user would read them. “Business account”, not accountType: "business"; the country name, not the ISO code; “Not provided” rather than a blank.
  • Offers one “Change” link per answer or group, labelled so a screen-reader user hears which answer it changes (“Change company name”, not five links called “Change”).
  • Returns to review after an edit. The user who changes their postcode should not have to click Next through three unaffected steps.
  • Revalidates before submit. Changing an earlier answer can make later answers invalid or irrelevant. Submit must check the whole path again, not only the step last edited.
The review step as a hub In the linear pattern, changing the first step's answer takes the user forward through step two and step three before reaching review again. In the hub pattern, each Change link opens the relevant step in edit mode, and saving it returns directly to review, after revalidating any answers that depended on it. Linear return Change → step 1 → Next → step 2 → Next → step 3 → Next → review. Three extra screens for one edit. Later steps may silently keep stale answers. Hub return Change → step 1 (edit mode) → Save → review. One screen for one edit. Dependent answers revalidated before returning.

The core pattern: an edit mode and a whole-path revalidation

type StepId = "account" | "company" | "address" | "tax";

interface WizardState {
  step: StepId | "review";
  returnTo: "review" | null;               // set when entering a step from a Change link
  answers: Record<StepId, Record<string, unknown> | undefined>;
}

type StepSchema = { safeParse(v: unknown): { success: boolean; error?: { issues: { path: (string | number)[] }[] } } };

export function changeFromReview(s: WizardState, step: StepId): WizardState {
  return { ...s, step, returnTo: "review" };
}

// Called when the user saves a step. In edit mode, go straight back to
// review; otherwise follow the normal path.
export function completeStep(
  s: WizardState, data: Record<string, unknown>, nextOnPath: (s: WizardState) => StepId | "review",
): WizardState {
  const updated = { ...s, answers: { ...s.answers, [s.step as StepId]: data } };
  if (s.returnTo === "review") {
    // An edit can put a NEW step on the path (e.g. switching to business adds
    // "company"). If that step has no answers yet, it must be visited first.
    const missing = firstIncompleteStep(updated);
    return missing ? { ...updated, step: missing } : { ...updated, step: "review", returnTo: null };
  }
  return { ...updated, step: nextOnPath(updated) };
}

// Whole-path validation run on entering review AND again on submit.
export function validatePath(
  s: WizardState, path: StepId[], schemas: Record<StepId, StepSchema>,
): { step: StepId; paths: string[] }[] {
  return path.flatMap((step) => {
    const r = schemas[step].safeParse(s.answers[step] ?? {});
    return r.success ? [] : [{ step, paths: r.error!.issues.map((i) => i.path.join(".")) }];
  });
}

declare function firstIncompleteStep(s: WizardState): StepId | null;
<dl class="review">
  <div class="review-row">
    <dt>Company name</dt>
    <dd>Analytical Engines Ltd</dd>
    <dd><a href="?step=company&amp;return=review">Change<span class="visually-hidden"> company name</span></a></dd>
  </div>
</dl>

Step-by-step walkthrough

  1. Render answers with a description list. <dl> with <dt> and <dd> pairs is read as term and value by screen readers and is easy to scan visually. Format values for humans, and show “Not provided” for optional blanks.
  2. Give each Change link a unique accessible name. Visually “Change”, audibly “Change company name”, using visually hidden text.
  3. Enter the step in edit mode. Record returnTo: "review", and label the step’s primary button “Save and return to review” so the user knows where it goes.
  4. Return to review after saving — unless the path grew. If the edit added a step that has never been answered, go there first; its button also returns to review.
  5. Revalidate the whole path on entering review. If an edit made a later answer invalid (a postcode no longer valid for the new country), show the review with that row flagged and its Change link as the obvious next action.
  6. Revalidate again on submit. Review may have been open for an hour; rules or server-side data may have changed. The final submit validates everything, and routes server errors back to rows on the review, not to hidden steps. The server side is covered in mapping 422 responses to field errors.
An edit that invalidates a later answer From review, the user activates Change country and the wizard opens the address step in edit mode. The user changes the country from Germany to France and saves. The wizard revalidates the whole path and finds the stored German VAT number invalid for France. It returns to review with the VAT row flagged and an error summary pointing at its Change link, instead of submitting an inconsistent record. User Wizard Path validator Change country (from review) address step, edit mode save: country DE → FR validate whole path tax.vatNumber invalid for FR review, VAT row flagged

Failure modes and edge cases

1. Review built from live form state instead of committed answers

If review reads the in-progress state of a step the user abandoned mid-edit, it shows values they never saved. Review renders committed answers only; an abandoned edit is discarded when the user returns without saving.

2. Irrelevant answers shown on review

After switching to a personal account, the company section should disappear from review — its answers are kept for convenience, but not shown or submitted. Use the same relevance rule as the payload, from what happens to errors when a field is hidden.

3. Focus on returning to review

Returning to review should put focus on the row that was changed (or the success message confirming it), not the top of the page — otherwise a screen-reader user must find their place again in a long list.

4. Sensitive values

Mask values such as full account numbers on review (“ending 4821”) and never echo passwords. Review pages are often screenshotted or left on shared screens.

5. Long reviews

For wizards with dozens of answers, group review rows under the step headings with a Change link per group rather than per row, and use the error summary pattern from building an accessible error summary when revalidation finds problems.

Formatting stored answers for review The raw value business is shown as Business account. The ISO code FR is shown as France. An empty optional field is shown as Not provided. A date stored as an ISO string is shown in the user's locale format. A full account number is masked to its last four digits. A boolean newsletter flag is shown as a sentence. Stored Shown on review accountType: "business" Business account country: "FR" France middleName: "" Not provided startDate: "2026-10-01" 1 October 2026 (user's locale) iban: "FR76…4821" Ending 4821 newsletter: false You will not receive the newsletter

Verification checklist


Frequently Asked Questions

Should the review step allow inline editing?

For a few simple fields, inline editing on review can be quicker. For anything with dependencies or validation beyond a single field, sending the user to the owning step keeps the validation and relevance logic in one place. Mixed approaches tend to duplicate rules.

Is a review step always worth adding?

It pays for itself when answers are consequential or hard to undo — applications, payments, legal declarations — and when the wizard is long enough that users cannot remember what they entered. For a two-step signup, a review step is friction.

Where should the final submit button's error go?

Server rejections of specific answers go on the matching review rows, with an error summary above them. Failures unrelated to any answer — outages, rate limits — go in a form-level banner at the top of review, and the submit button stays available for retry.


Related

← Multi-Step Form State Machines