Docs

MultiStep

One page at a time with a progress line, Back and Next, and Submit on the last page. Covers MultiStep, Page and Progress.

A schema with more than one page renders as a multi-step form: Progress, the current Page, Back/Next buttons, and Submit on the last page. Progress is saved to Mirio after each completed step as a partial submission.

Preview

Usage

// A schema with several pages renders as MultiStep automatically:
<Form schema={demo} client={mirio} />

// The same, composed:
import { Form, MultiStep, Page, Progress, Field } from '@trymirio/mirio';

<Form schema={demo} client={mirio}>
  <Progress />
  <MultiStep notice="We save your progress after each step." />
</Form>

// Custom content per step (children replace the current page's fields):
function Steps() {
  const { pageIndex } = useForm();
  return (
    <MultiStep>
      {pageIndex === 0 && <><Field name="name" /><Field name="email" /></>}
      {pageIndex === 1 && <Field name="timeline" />}
    </MultiStep>
  );
}

Pages with a showWhen condition that is false, or whose fields are all hidden, are skipped by Next/Back and left out of the step count. See Conditional logic.

Props

MultiStep

Prop Type Default Description
nextLabel ReactNode "Next" Label of the Next button.
backLabel ReactNode "Back" Label of the Back button.
notice ReactNode | null none A line under the buttons, such as "We save your progress after each step."
children ReactNode the current page's fields Rendered inside the current Page instead of its fields.
showProgress boolean true Render the progress line at the top.
progress ReactNode <Progress /> Replaces the built-in progress element (e.g. <Progress label={(s, c) => ${s}/${c}} /> or your own stepper).
className string flex flex-col gap-mirio Merged onto the root.
…<div> attributes Spread on the root.

Renders <div data-mirio="multistep"> containing the progress element, the current Page, <div data-mirio="nav"> with [data-mirio="back"] (secondary Button, hidden on the first visible page) and [data-mirio="next"] (a type="submit" button, so Enter advances) or Submit with fullWidth={false} on the last visible page, and <p data-mirio="notice"> when there's a notice.

Page

Prop Type Default Description
index number the current page Which page of schema.pages to render.
headingLevel 2 | 3 | 4 2 Element used for the page title.
children ReactNode the page's visible fields as Fields Replaces the fields.
className string flex flex-col gap-mirio Merged onto the root.
…<div> attributes Spread on the root.

Renders <div data-mirio="page"> with an optional <div data-mirio="page-header"> holding <h2 id tabindex="-1">title</h2> (or h3/h4 per headingLevel) and a description, then the fields. Renders nothing when the page is hidden.

Progress

Prop Type Default Description
label (step: number, count: number) => string `Step ${step} of ${count}` Custom text; step is 1-based and both count visible pages only.
className string flex items-center gap-3 text-sm text-mirio-muted-fg Merged onto the root.
…<div> attributes Spread on the root.

Renders <div data-mirio="progress"><span>Step 2 of 3</span><div role="progressbar" aria-valuemin="1" aria-valuemax="3" aria-valuenow="2" aria-valuetext="Step 2 of 3"><div style="width: 66%"/></div></div>.

Variants and states

  • First page: no Back button (a spacer keeps Next on the right).
  • Last visible page: Submit replaces Next.
  • Validation: Next validates only the current page's visible fields; on failure the first invalid control is focused and the page stays. Submit validates every visible field and jumps to the first page with an error.
  • Saving: after each successful Next, the completed pages are sent as a partial (status: 'partial', same session id kept in sessionStorage); the final submit completes the same record. Nothing is saved on keystrokes.

Customization

  • Tokens: --mirio-space (gaps), --mirio-muted (progress track), --mirio-primary (progress bar), --mirio-muted-fg (progress text and notice), plus the button tokens.
  • notice="…" adds a line under the buttons; showProgress={false} removes the progress line; progress={<Progress label={(s, c) => ${s}/${c}} />} keeps it but shortens the text.
  • CSS hooks: [data-mirio="multistep"], [data-mirio="progress"], [data-mirio="page"], [data-mirio="page-header"], [data-mirio="nav"], [data-mirio="back"], [data-mirio="next"], [data-mirio="notice"].
  • For a wizard with a sidebar or horizontal steps, use usePage() and useForm().visiblePages from the headless layer and render your own chrome around Page.

Accessibility

On page change focus moves to the page heading (tabindex="-1") or to the first field when the page has no title, so screen readers announce the new step. Next is a submit button so Enter advances; Back is type="button". The progress bar exposes role="progressbar" with min/max/now and a text value.

Form, Submit, Conditional logic, Partial submissions, Headless: usePage