Confirm page
The three-step confirm flow every Japanese business app ships — and
plenty elsewhere call review-before-commit. This template is the
multi-step form
machinery specialised to its most common shape: an input step
(field errors on 422),
a review step the server renders from what it parsed, and a done
step that is safe to double-submit because the confirm render minted
an idempotency key.
One region, whole-region swaps, an
hc-stepper re-rendered
as part of the same truth. Zero new JS.
Live template
Section titled “Live template”Submit the form to reach Review — the amount you typed comes back
formatted, because the review shows the server’s parse, not an echo.
Back returns to the input step with your values intact (a named
submit, not history.back()). Place order lands on Done;
press it twice and you get the same receipt number — the response
derives from the idempotency key, which is the replay contract. Typing
a non-numeric amount shows the 422 path.
Loading flow…
Page skeleton
Section titled “Page skeleton”The complete flow, with real-app-shaped URLs. Every step is the same
region (#flow) swapped outerHTML; the stepper rides in each
response so state and presentation can never disagree.
<!-- Step 1: input --><section id="flow"> <ol class="hc-stepper"> <li class="hc-stepper__step" aria-current="step"> <span class="hc-stepper__marker" aria-hidden="true">1</span> <span class="hc-stepper__label">Input</span> </li> <li class="hc-stepper__step">…Review…</li> <li class="hc-stepper__step">…Done…</li> </ol> <form method="post" action="/orders/confirm" data-hx-post="/orders/confirm" data-hx-target="#flow" data-hx-swap="outerHTML" data-hx-disabled-elt="find button[type=submit]"> <!-- …hc-field inputs… 422 re-renders this step with field errors --> <button class="hc-button" data-variant="primary" type="submit">Review</button> </form></section>
<!-- Step 2: review (a /confirm response) --><section id="flow"> <ol class="hc-stepper"> <li class="hc-stepper__step" data-state="complete"> <span class="hc-stepper__marker" aria-hidden="true">✓</span> <span class="hc-stepper__label">Input</span> </li> <li class="hc-stepper__step" aria-current="step">…Review…</li> <li class="hc-stepper__step">…Done…</li> </ol> <form method="post" action="/orders/place" data-hx-post="/orders/place" data-hx-target="#flow" data-hx-swap="outerHTML" data-hx-disabled-elt="find button[type=submit]"> <input type="hidden" name="idempotency_key" value="ik_7d1f9c2e"> <input type="hidden" name="item" value="Ergonomic chair"> <input type="hidden" name="amount" value="48000"> <dl class="hc-stack"> <div><dt>Item</dt><dd>Ergonomic chair</dd></div> <div><dt>Amount</dt><dd>¥48,000</dd></div> </dl> <div class="hc-toolbar" role="toolbar" aria-label="Review actions"> <button class="hc-button" type="submit" name="nav" value="back" formnovalidate>Back</button> <button class="hc-button" data-variant="primary" type="submit" name="nav" value="place">Place order</button> </div> </form></section>
<!-- Step 3: done (a /place response; PRG in the full-page flow) --><section id="flow"> <ol class="hc-stepper"> <li class="hc-stepper__step" data-state="complete">…Input ✓…</li> <li class="hc-stepper__step" data-state="complete">…Review ✓…</li> <li class="hc-stepper__step" aria-current="step">…Done…</li> </ol> <div class="hc-card" role="status"> <p>Order <strong>REQ-58214</strong> placed — ¥48,000.</p> </div></section>Wiring map
Section titled “Wiring map”| Region | Component / block | Recipe | Contract |
|---|---|---|---|
| The whole flow | one #flow region, outerHTML swaps | multi-step-form | contract |
| Stepper | hc-stepper, aria-current="step", complete markers server-drawn | — | rendered per step in the same swap |
| Input step 422 | hc-field + aria-invalid + error hints | field-errors | contract |
| Review step | <dl class="hc-stack"> read-back + hidden fields | — | the server renders what it parsed — the read-back is the point |
| Back | a named submit (name="nav" value="back", formnovalidate) | multi-step-form | never history.back() — the server owns the truth |
| Done / double-submit | key minted at the review render | idempotency-key | contract |
Three rules carry the design:
- The review is the server’s read-back. What the user confirms is what the server understood — parsed, formatted, normalized. An echo of the client’s strings would confirm nothing.
- The values ride as hidden fields in the review form: stateless,
inspectable, and exactly what
/placewill re-validate (it must — hidden fields are still client-supplied). - The key is minted at the review render: going Back and forward again mints a fresh one (a new intent), while double-clicking Place order replays the same one (the same intent).
Adapt it
Section titled “Adapt it”- Rename
/orders/confirm//orders/placeto your resource; in the full-page (no-JS) flow,/placeanswers with a303redirect to the receipt page (PRG) — the stored response for a replayed key is that same redirect. - The review step is the natural home for terms-of-service checkboxes
and
hc.print.css— people print confirm pages. - Long forms: compose the input step with autosave and unsaved changes; the review + done steps are read-only and need neither.
- Multi-page inputs: chain more input steps exactly as multi-step form documents; keep the review as the second-to-last step.