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 Kibble after each completed step as a partial submission.
Preview
Usage
// 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.
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 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 Fields |
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 insessionStorage); 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()anduseForm().visiblePagesfrom the headless layer and render your own chrome aroundPage.
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, Submit, Conditional logic, Partial submissions, Headless: usePage