Questionnaire Multi-step Form
A step-driven form flow orchestrator: step header + progress ("Step {current} of {total}") + per-step content panels + per-step validation gating. Ideal for wizards, surveys, and multi-step checkout. Validation reuses the oas-form kernel — place one <oas-form> inside each step panel; moving forward validates the current step and blocks on failure, while going back never validates and panels stay mounted (filled values are preserved).
Scope: linear gating + optional-step skip + host-defined jumps via
oas-before-change+ conditional branching via the host composition channel (hiddendata bit +oas-before-change+oas-values-change, see below). No predicate / expression engine is built in — the host owns the full answer data, keeping branch logic testable and vetoable at the application layer. Start/complete pages are not built in; implement draft persistence yourself viacurrent+getValues().
Basic usage
Pass a steps array ({ key?, title, description?, optional?, hidden? }) and attach per-step content via named slots: slot="step-<key>" for keyed steps, slot="step-<index>" otherwise. The primary button becomes "Submit" on the last step; clicking it re-validates every participating step, then emits oas-submit (detail.values merged across steps). Values and validation share one scope — only participating steps (not hidden, not skipped) count; hosts needing the full data can read the inner oas-form of each panel directly.
Basic multi-step questionnaire
Click "Submit" when everything looks good.
Per-step validation gating
Before advancing, the current step's oas-form is validated (via its validate()); failure blocks navigation and emits oas-step-validate (detail: { index, key?, valid, errors }). Going back never validates. Set validation="false" for pure navigation.
Blocked until valid
You can only reach this step after validation passes.
Optional steps (skip)
A step with optional: true shows a "Skip this step" button: skipping bypasses validation and advances directly, emitting oas-skip (detail: { index, key? }). A skipped step leaves the participating set — its values drop out of getValues() / oas-submit and it no longer takes part in submit re-validation; revisiting the step (back / header click / goto()) restores participation, and reset() clears all skip records.
Optional step (Skip)
Final step.
Host interception (oas-before-change)
Before any jump (buttons / header clicks / next() / prev() / goto() / skip) a cancelable oas-before-change fires (detail: { index, key?, from }); calling preventDefault() vetoes the jump — also the hook for custom branch navigation. With linear="false", every header step becomes clickable.
preventDefault veto
Step 1 content
Step 2 content
Confirm page (blocked by the host in this demo)
Conditional branching (skip by answer)
Conditional branching uses the host composition channel — no built-in predicate engine: listen for oas-values-change to read the answer, then rewrite steps to flip the hidden bit. Hidden steps leave the header and progress, next() skips them automatically, and their values drop out of the values/validation scope. If the flipped step is the one the user is on, the component aligns current to the nearest visible step and emits oas-change (no manual jump needed). Keeping branch logic in the host means: testable rules, an extra veto point in oas-before-change, and no expression language re-invented inside the component.
Hide/restore steps by answer
Final step.
Step transition (animated)
animated enables the step transition: the incoming panel slides in along the navigation direction (forward from the inline-end side, backward from the inline-start side) while fading in — transform/opacity only, compositor-friendly; users with prefers-reduced-motion: reduce automatically get no animation. Override the duration via the CSS variable --oas-questionnaire-anim-duration (defaults to --oas-transition-base); the slide direction mirrors automatically in RTL layouts.
animated transition (click Next/Previous to watch the slide direction)
Click "Next": this panel slides in along the navigation direction.
Forward and backward slide from opposite sides; use "Previous" to watch the return.
Final step.
Keyboard shortcuts
shortcuts enables keyboard step navigation: Alt+←/→ works anywhere inside the component (including inside inputs) and swallows the default action (preventing browser history navigation); bare ←/→ (no modifiers) only switches steps when focus is not inside an input/textarea/editable region (never hijacks the caret or control-specific keyboard handling) — clicking panel whitespace focuses the component container, where bare arrows also work. Triggers reuse the same gating chain as the buttons (a failing current step blocks advance); physical mapping (→ forward / ← back), not mirrored in RTL, matching the browser history-key convention.
shortcuts (click into the component, then try Alt+←/→)
With focus on panel whitespace, bare ←/→ also switch steps.
Final step.
Controlled current & methods
current is two-way: internal jumps write it back, external updates sync instantly. Imperative methods next() / prev() / goto(index) / validate() / submit() / getValues() / reset() cover all navigation and value scenarios.
Event naming note: events from fields inside step panels (
oas-change/oas-input, etc.) bubble out through the questionnaire. A field'soas-changeshares the name with the step-changeoas-change— when listening for step changes, filter bydetail.index(field events carry noindex); for field value changes preferoas-values-change(detail: { name, value, values }).
External control & values
Progress variants
progress-variant: both (default, text + bar) / text / bar; progress="false" hides the region. The bar uses role="progressbar" + aria-valuenow/min/max for screen readers.
progress-variant & hidden
both (default)
Text + bar
C
text
Progress text only
C
bar
Progress bar only
C
Value retention & reset
Panels stay mounted (only hidden toggles): going forward then back keeps filled values. reset() restores every step to its initial values, clears errors, returns to step 0, and dispatches no events.
Values kept on back + reset()
Click "Previous" — your edit is still there.
Custom navigation (hide-header / hide-nav)
hide-header / hide-nav hide the built-in step header / nav region so the host can fully compose its own (progress and panels are still driven by the component).
hide-nav + external buttons
Built-in nav hidden
Driven by external buttons
Pure navigation (validation / progress / hide-header)
validation="false" disables per-step gating (pure navigation — empty required fields still pass); progress="false" hides the progress region; hide-header hides the built-in step header. Each is independent, and they can be combined into a minimal flow (only panels and nav buttons remain).
Pure navigation: no gating / no progress / no header
You reach this step even with the field empty (gating is off).
API
Methods
| Method | Description |
|---|---|
next() | Next step: validates the current step's oas-form first (skipped when validation="false"), advances only on success; re-entrant calls while async validation is pending are rejected. Returns Promise<boolean> |
prev() | Previous step: never validates (going back is never blocked). Returns boolean |
goto(index) | Jump to any step without the gating; out-of-range indexes are clamped, hidden steps resolve forward. Returns boolean |
validate() | Validates every participating step (non-hidden, non-skipped; sequentially, emitting oas-step-validate per step), returns Promise<boolean> |
submit() | Submit: re-validates every participating step (including earlier ones), emits oas-submit (detail.values merged across participating steps only — hidden / skipped steps excluded) only when all pass. Returns Promise<boolean> |
getValues() | Merged values across participating steps (same set as validation): each participating step's oas-form current values deep-merged in step order (same shape as a single form's submit() detail.values; zero-interaction fields prefilled via the value attribute are included); read the inner oas-forms for the full data. Returns Record<string, unknown> |
reset() | Resets every step's form to its initial values, clears errors, sets current to 0; dispatches no events |
oas-questionnaire
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
animated | Step transition (opt-in, on when present and not "false"): the incoming panel slides in along the navigation direction while fading in (transform/opacity only); automatically disabled for prefers-reduced-motion: reduce; slide direction mirrors in RTL | boolean | — |
current | Current step index (0-based, two-way: internal jumps write back, external updates sync instantly); invalid values fall back to 0, out-of-range clamped | string | 0 |
finish-text | Last-step primary button label (overrides the locale default "Submit") | — | — |
hide-header | Hide the built-in step header (host composition) | boolean | — |
hide-nav | Hide the built-in nav region (host composition) | boolean | — |
linear | Linear mode (on by default, linear="false" disables): future steps are not clickable in the header; when disabled, any step is clickable (still intercepted by oas-before-change) | string | true |
next-text | "Next" button label (overrides the locale default) | — | — |
prev-text | "Previous" button label (overrides the locale default) | — | — |
progress | Progress region visibility (progress="false" hides it) | string | true |
progress-variant | Progress variant: both (default, text + bar) / text / bar; invalid values fall back to both | string | both |
shortcuts | Keyboard step navigation (opt-in, on when present and not "false"): Alt+←/→ switches steps anywhere inside the component (swallows the default to prevent browser history navigation); bare ←/→ only when focus is outside inputs/editable controls (never hijacks the caret) | boolean | — |
size | Size tier: xs/small/medium/large/xl (title font density; invalid values fall back to medium with a one-time warning (deduped by identical value)) | string | medium |
skip-text | "Skip this step" button label (overrides the locale default) | — | — |
steps | Step data JSON [{ key?, title, description?, optional?, hidden? }]; invalid/empty falls back to [] | QuestionnaireStep[] | string | [] |
validation | Per-step validation gating (on by default, validation="false" for pure navigation; submit() always re-validates regardless) | string | true |
Events
| Event | Description |
|---|---|
oas-before-change | Fires before any jump (buttons / header clicks / next / prev / goto / skip), cancelable; detail: { index, key?, from } — preventDefault() vetoes the jump |
oas-change | After a successful step switch; detail: { index, key? } |
oas-skip | When an optional step is skipped; detail: { index, key? } (skipping never validates; the skipped step leaves the values/validation scope until revisited) |
oas-step-validate | After a per-step validation; detail: { index, key?, valid, errors } |
oas-submit | Finished on the last step (all participating steps validated); detail: { values } (merged across participating steps only — hidden / skipped steps excluded) |
oas-values-change | Natural bubble-through of the inner oas-form value changes (detail: { name, value, values }, values is that step's snapshot); use getValues() for the merged view |
Slots
| Name | Description |
|---|---|
step-<index> | Step content panel (unkeyed steps associate by array index) |
step-<key> | Step content panel (keyed steps); place one <oas-form> inside to carry the step's fields and rules |
CSS Variables
| CSS Variable | Description | Default |
|---|---|---|
--oas-questionnaire-anim-duration | Step transition duration (applies when animated is on) | var(--oas-transition-base, 180ms) |
--oas-questionnaire-nav-gap | Nav region button gap | var(--oas-space-2) |
--oas-questionnaire-progress-bar-bg | Progress fill color | var(--oas-color-primary) |
--oas-questionnaire-progress-bg | Progress track background | var(--oas-color-bg-hover) |