Multi-Step Form

Split a long form into steps the visitor moves through one at a time, validating each step before the next one opens.

Multi-Step Form is an add-on for Forms. It adds four blocks to the form editor which let you break one long form into a sequence of steps. The visitor sees one step at a time and moves through them with Next and Previous buttons.

It stays one form and one submission. Every answer, from every step, is held in the page and sent together the moment the visitor presses Submit on the last step — nothing is sent step by step, and going back to change an earlier answer simply changes it. So a visitor who abandons the form halfway leaves nothing behind, and you get either the whole thing or nothing at all.

Where to find it

Nowhere but the form editor. This plugin has no settings screen and nothing to configure site-wide — the whole feature is the four blocks and their sidebar settings. Two of them (Steps and Step) sit in the inserter's Utils category, the two buttons in Actions.

Forms has to be active

The plugin does nothing on its own. Without XPAC Forms it registers no blocks at all and shows an admin notice asking for Forms instead.

The four blocks

BlockWhat it is
StepsThe container that holds the whole sequence. One per form.
StepOne step. Put your fields inside it. Only goes inside Steps.
Next buttonMoves the visitor forward one step. One per form.
Previous buttonMoves the visitor back one step. One per form.

The two buttons are ordinary form buttons for styling purposes — colour, border, padding, font and a Fill or Outline style, all from the block sidebar, exactly like the Submit button.

Building a stepped form

Edit the form and insert the Steps block. It arrives with one empty Step already inside it.

Click into that step and add your fields. Start with a Heading that names the step, and use Columns, Groups and Paragraphs to organise it when needed.

Add the next step with the Add Step button, in the block toolbar and again in the block sidebar. There is deliberately no + inside the Steps block, so this button is the only way to add one.

Insert a Next button and a Previous button, and place them after the Steps block, not inside a step. See the warning below — this is the one thing that is easy to get wrong and it breaks the form.

Move your Submit button into the last step, so it only appears once the visitor has got there.

Update the form.

The Next and Previous buttons must sit outside the Steps block

Each step hides the others, so a Next button that lives inside step 1 vanishes the instant the visitor arrives at step 2 — and you are allowed only one Next and one Previous button per form, so you cannot add a spare. The symptom is a visitor stranded on step 2 with no way forward and no way back.

This is easy to do by accident, because inserting a block while you are editing a step drops it into that step. The Steps sidebar now warns when it finds either navigation button nested there. Put both buttons below the Steps block, and click a step to check the buttons are still visible.

Leave the Submit button in the last step

Nothing hides the Submit button on the way through. If it sits outside the Steps block it is visible from step 1, and a visitor can press it before they have seen the rest of your questions. Required fields still stop them — the form refuses and jumps them to the first step that has a problem — but on a form where everything is optional you will collect half-finished submissions.

A Steps block with no Next button is a dead end

When a Steps block has more than one step and the form has no Next button, its sidebar warns you. The warning cannot repair an already published form: the visitor still sees step 1 and can never leave it. If a form stops advancing after an edit, check the Next button is still there and still outside the Steps block.

What the visitor sees

Only the current step. The others are hidden, and hidden fields are skipped when tabbing through the form, so keyboard visitors are not walked through invisible questions.

Next checks the step before it opens the following one. Every field on the current step is validated — required fields, email and URL formats, lengths, all of it — and if anything fails, the step does not change and the messages appear beside the offending fields. A visitor cannot click through five steps and discover their problems at the end.

Previous never blocks. The visitor can always go back, and their answers on the step they leave are kept.

The buttons manage themselves: Previous is hidden on the first step and Next on the last, so the visitor is not offered a direction that does not exist.

If the site's own checks reject something after the visitor presses Submit, the form jumps back to the first step that contains a rejected field so they can see it. And after a successful submission the form returns to step 1, ready for the next person.

Above the steps, a visible Step 2 of 5 status tells the visitor where they are. It is a live screen-reader announcement, and both numbers recalculate when skip rules remove or restore a step. Each step wrapper also exposes the same text as its accessible name. Give the step a real Heading as well: the generated count supplies position, not the subject of the questions.

Reloading the page starts again from the top

Nothing about the visitor's position is remembered. A reload puts them back on step 1, and the browser's Back button leaves the form altogether rather than going back a step — with everything they had typed. Keep steps short enough that this is not a catastrophe, and do not rely on it for long applications.

Focus and the announcement move with the visitor

Next and Previous focus the newly shown, labelled step and let the browser scroll it into view. If Next finds an invalid answer, it keeps the step open and focuses the first usable invalid control. A failed final submission does the same after returning to the first rejected step. The live count announces every position change. Keep a clear Heading first in every step and still test keyboard and screen-reader navigation with your theme.

Skipping a step

Any step can be given a rule that takes it out of the sequence when it is not relevant — ask about a delivery address only if the visitor chose delivery, for instance.

This part needs the Conditional Logic plugin

Skip rules are built with Conditional Logic's condition editor. Without that plugin active the button described below simply is not there.

Select the step and open the block sidebar.
Press Add skip logic.

Choose whether All or Any of your conditions must match, then add them. Each one names a field, a comparison and a value.

Save the rule and update the form.

The rule says when to skip the step, not when to show it. When it matches, the step is dropped from the sequence: the visitor never sees it, Next jumps straight over it, its answers are not submitted, not stored and not emailed, and any required fields inside it stop being required. Rules are re-checked as the visitor types, so a step can appear or disappear while they are still working.

Password and Confirmation controls are deliberately unavailable as skip-rule sources. Old or hand-edited rules that still name one are treated as unresolved in both the browser and server, so a write-only answer cannot become a visible oracle or decide which data is accepted.

Never let your rules skip every step

If every step's rule matches at once, the Steps area and its Next/Previous navigation are hidden, every Submit control on the form is disabled, and the live status says No form steps are currently available. That prevents an empty submission but leaves the visitor at an unusable form. Always leave at least one step that no rule can remove.

Rules based on earlier answers are easiest to follow

When a changed answer skips the current step, the form moves the visitor to the next available step, or the last available step when there is none later. This avoids a dead end, but rules that depend on the same or a later step can still make the form jump unexpectedly. Prefer answers the visitor has already completed.

Re-open your skip rules after deleting a field

A rule remembers the field it was pointed at by name. Delete or rename that field and that branch can no longer match. An All rule then leaves the step visible; an Any rule can still skip it when another valid branch matches. This fails safely, but it may no longer implement your intended journey. After removing a field, check the skip rules of every step.

Turning Conditional Logic off leaves the rules in place but inert

Deactivate that plugin and every skip rule you have saved stops being applied, with a warning only in the browser console and no owner-facing admin notice. Steps you meant to hide are shown to everybody, and the questions inside them become required again. The rules are not lost — they start working again as soon as it is reactivated.

A step can end up empty

Hiding a field with Conditional Logic's own field rules does not remove the step that contains it. Hide every field on a step that way and the visitor gets a step with nothing on it but the buttons. To remove a whole step, use the step's own skip logic rather than hiding its fields one by one.

For developers

This is a blocks plugin, and unusually thin on PHP. It fires no hooks of its own, registers no REST routes, stores no options and adds no post meta — there is nothing for a hook reference to document, which is a fact about the plugin rather than a gap in these docs. It listens on two Forms-side hooks and everything else it does is in JavaScript and CSS.

Blocks

All four are registered in Addon::init() on xpac_forms_init, from dist/packages/multi-step-form/<block>, and then handed to Blocks::registerFormBlock() with a structureProvider only. None of them declares is_changeable, so none holds a value: they never appear in field-mapping dropdowns and carry no validation rules of their own.

BlockNameCategoryNotes
Stepsxp/form-field-stepsxpac-form-util-fieldmultiple: false; align, __experimentalLayout; children restricted to xp/form-field-step
Stepxp/form-field-stepxpac-form-util-fieldparent: ["xp/form-field-steps"]; any inner blocks
Next buttonxp/form-field-step-nextxpac-form-action-fieldmultiple: false; renders a button[type=button]
Previous buttonxp/form-field-step-prevxpac-form-action-fieldmultiple: false; renders a button[type=button]

xp/form-field-steps declares no attributes at all, on purpose: its editor and save components read none. The align and layout attributes it serialises are the ones core injects from supports.

AttributeOnTypeDefault
conditionxp/form-field-stepobjectrelation (and/or) and dependencies (name, operator, value){"relation":"and","dependencies":[]}
labelboth buttonsstring, edited as RichText"Next" / "Prev"
additionalAttributesboth buttonsobject, applied through Forms' useFieldOutputProps{}

The step's condition is serialised into a data-skip-conditions attribute on the saved wrapper by step/components/SaveBlock.js, and the front-end runtime deletes that attribute from the DOM as soon as it has read it.

Both buttons set selectors.root to .xp-block-form-field--next button / .xp-block-form-field--prev button and skip serialisation for colour, typography, spacing and border, so their block-supports styles arrive from the generated stylesheet and the rendered button carries no inline style of its own. The runtime adds is-xpac-step-hidden when a direction is unavailable; CSS hides both that button and its wrapper through the class.

Front end

steps/view.js is the whole runtime. It registers through Forms' registerFormInitializer(), so it runs for forms present at page load and ones inserted later. Steps are .xp-block-form-field--step, hidden by default and shown with current. There is no history entry or session storage. Every step receives tabindex="-1", role="group", aria-hidden, a Step n of total accessible name and aria-current="step" while active. A generated paragraph before the Steps block shows the same count with role="status", aria-live="polite" and aria-atomic="true". Navigation focuses the newly active wrapper with scrolling enabled; local and server-side validation focus the first usable invalid control in the step they keep or reopen.

  • Step validation. changeStep() collects input, textarea, select under the active step, takes a new FormData(form) snapshot, and calls form.xpacForm.validation.validateField(name, value) for each — Forms' own validator, which renders the message beside the field as a side effect. Names in getDisabledRules() are skipped. Only Next performs this check; Previous neither blocks nor paints errors. On a failure the first usable invalid control receives focus.
  • Skip logic. The condition attribute is read from data-skip-conditions and registered through form.xpacForm.condition.initializeField, which is contributed by Conditional Logic's xp/form-field-conditional view script. Addon::init() adds that script as a dependency of the steps view script when the conditional block is registered. A matching rule sets the step's isDisabled and disables its inner fields through Forms' owner-aware field.setDisabled() API, so removing the skip reason cannot undo a different conditional reason. If a rule disables the active step, reseatActiveStep() moves and focuses the next surviving step, or the last survivor. With no survivor it hides the Steps container and both navigation wrappers, disables every Submit control and changes the progress status to an alert. Initial condition hydration never steals page-load focus; that focus move is reserved for a later rule change caused by the visitor. Malformed JSON, a missing Conditional Logic API and rule-initialization exceptions each produce a scoped console.warn; stepping still works with the affected rules inert.
  • Reset. A success callback marks the form for reset; a finally callback then re-triggers skip conditions and returns it to the first surviving step after the rest of Forms' success callbacks have settled.

Server side

Addon::editSubmissionInvisibleBlocks() is on xpac_forms_submission_invisible_blocks, a filter fired inside Conditional Logic's removeConditionalData() during xpac_forms_initialize_submission. It re-evaluates each step's condition against the submitted values with ConditionalLogic::doesValueMatch() and appends the changeable inner blocks of every matching step to the invisible list; Conditional Logic then array_diff_keys those names out of both the submission data and the validation rules. Because removeConditionalData() calls itself once recursively, the callback runs twice per submission.

The filter is Conditional Logic's, so with that plugin inactive this callback is never registered anywhere it could run and step skip logic has no server-side half. When it does run, browser and server resolve each dependency against current submitted controls/data and registered provider values. For an AND relation, one unresolved name makes the whole rule non-matching. For OR, unresolved names are ignored and the known dependencies may still match; if all are unresolved, the rule is non-matching. A present but unchecked optional control is still resolved — its empty value is evaluated normally. A malformed condition, relation or dependency shape is also non-matching server-side. This keeps a deleted or renamed field from turning a stale != rule into silent data loss.

On this page