Skip to content

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 (hidden data 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 via current + 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

AlipayWeChat Pay

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

Invoice neededNo invoice

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.

reset()

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's oas-change shares the name with the step-change oas-change — when listening for step changes, filter by detail.index (field events carry no index); for field value changes prefer oas-values-change (detail: { name, value, values }).

External control & values

prev()next()goto(2)validate()getValues()reset()

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.

reset()

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

PreviousNext

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 ​

MethodDescription
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 ​

AttributeDescriptionTypeDefault
animatedStep 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 RTLboolean—
currentCurrent step index (0-based, two-way: internal jumps write back, external updates sync instantly); invalid values fall back to 0, out-of-range clampedstring0
finish-textLast-step primary button label (overrides the locale default "Submit")——
hide-headerHide the built-in step header (host composition)boolean—
hide-navHide the built-in nav region (host composition)boolean—
linearLinear 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)stringtrue
next-text"Next" button label (overrides the locale default)——
prev-text"Previous" button label (overrides the locale default)——
progressProgress region visibility (progress="false" hides it)stringtrue
progress-variantProgress variant: both (default, text + bar) / text / bar; invalid values fall back to bothstringboth
shortcutsKeyboard 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—
sizeSize tier: xs/small/medium/large/xl (title font density; invalid values fall back to medium with a one-time warning (deduped by identical value))stringmedium
skip-text"Skip this step" button label (overrides the locale default)——
stepsStep data JSON [{ key?, title, description?, optional?, hidden? }]; invalid/empty falls back to []QuestionnaireStep[] | string[]
validationPer-step validation gating (on by default, validation="false" for pure navigation; submit() always re-validates regardless)stringtrue

Events ​

EventDescription
oas-before-changeFires before any jump (buttons / header clicks / next / prev / goto / skip), cancelable; detail: { index, key?, from } — preventDefault() vetoes the jump
oas-changeAfter a successful step switch; detail: { index, key? }
oas-skipWhen an optional step is skipped; detail: { index, key? } (skipping never validates; the skipped step leaves the values/validation scope until revisited)
oas-step-validateAfter a per-step validation; detail: { index, key?, valid, errors }
oas-submitFinished on the last step (all participating steps validated); detail: { values } (merged across participating steps only — hidden / skipped steps excluded)
oas-values-changeNatural 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 ​

NameDescription
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 VariableDescriptionDefault
--oas-questionnaire-anim-durationStep transition duration (applies when animated is on)var(--oas-transition-base, 180ms)
--oas-questionnaire-nav-gapNav region button gapvar(--oas-space-2)
--oas-questionnaire-progress-bar-bgProgress fill colorvar(--oas-color-primary)
--oas-questionnaire-progress-bgProgress track backgroundvar(--oas-color-bg-hover)

Released under the MIT OR Apache-2.0 License.