---
title: MultiStep
description: One page at a time with a progress line, Back and Next, and Submit on the last page. Covers MultiStep, Page and Progress.
preview: multi-step
---
# MultiStep

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

## Preview
<!-- preview: multi-step -->

## Usage
```tsx
// A schema with several pages renders as MultiStep automatically:
<Form schema={demo} client={kibble} />

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

<Form schema={demo} client={kibble}>
  <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](/docs/conditional-logic.md).

## 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-kb` | Merged onto the root. |
| …`<div>` attributes | | | Spread on the root. |

Renders `<div data-kb="multistep">` containing the progress element, the current `Page`, `<div data-kb="nav">` with `[data-kb="back"]` (secondary `Button`, hidden on the first visible page) and `[data-kb="next"]` (a `type="submit"` button, so Enter advances) or [Submit](/docs/components/submit.md) with `fullWidth={false}` on the last visible page, and `<p data-kb="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 `Field`s | Replaces the fields. |
| `className` | `string` | `flex flex-col gap-kb` | Merged onto the root. |
| …`<div>` attributes | | | Spread on the root. |

Renders `<div data-kb="page">` with an optional `<div data-kb="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-kb-muted-fg` | Merged onto the root. |
| …`<div>` attributes | | | Spread on the root. |

Renders `<div data-kb="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: `--kb-space` (gaps), `--kb-muted` (progress track), `--kb-primary` (progress bar), `--kb-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-kb="multistep"]`, `[data-kb="progress"]`, `[data-kb="page"]`, `[data-kb="page-header"]`, `[data-kb="nav"]`, `[data-kb="back"]`, `[data-kb="next"]`, `[data-kb="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.

## Related
[Form](/docs/components/form.md), [Submit](/docs/components/submit.md), [Conditional logic](/docs/conditional-logic.md), [Partial submissions](/docs/partials.md), [Headless: usePage](/docs/components/headless.md)
