Wizard step
One step of a flow that asks the crew for something, with a title, its content, and a footer holding a note and the step's buttons.
On this page
Add a maintenance record
Choose how to start. You can change everything later.
Code
import { Button } from "@bosunkit/components/button";
import { RadioCards } from "@bosunkit/components/radio-cards";
import { FormField } from "@bosunkit/components/form-field";
import { TextArea } from "@bosunkit/components/text-area";
import { WizardStep } from "@bosunkit/components/wizard-step";
import { FilePlus, LayoutTemplate, Sparkles } from "lucide-react";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"The first step of adding a maintenance record: how to start, what the job is, examples to try, and a footer that says " +
"what will happen with Cancel and the one primary action.";
const EXAMPLES: readonly string[] = [
"Replace the raw-water impeller every 200 engine hours",
"Check the liferaft service date before each season",
"Grease the winches every month",
];
export default function Showcase(): ReactElement {
const [description, setDescription]: [string, Dispatch<SetStateAction<string>>] = useState<string>(
"Replace the engine's raw-water impeller every 200 engine hours and remind me a week before.",
);
return (
<WizardStep
title="Add a maintenance record"
description="Choose how to start. You can change everything later."
closeLabel="Close"
onClose={(): void => undefined}
note={
<span style={{ display: "inline-flex", alignItems: "center", gap: "var(--space-sm)" }}>
<Sparkles size="1em" aria-hidden="true" /> Drafts are saved as not yet due
</span>
}
actions={
<>
<Button>Cancel</Button>
<Button variant="primary">Create record</Button>
</>
}
>
<RadioCards
label="How do you want to start?"
defaultValue="describe"
options={[
{ value: "describe", label: "Describe it", description: "Bosun drafts the steps from plain words.", icon: <Sparkles size="1.25em" /> },
{ value: "template", label: "From a template", description: "Tested schedules for your engine.", icon: <LayoutTemplate size="1.25em" /> },
{ value: "blank", label: "Blank record", description: "Fill in every field yourself.", icon: <FilePlus size="1.25em" /> },
]}
/>
<FormField label="What needs doing?">
<TextArea
aria-label="What needs doing?"
value={description}
onChange={(event): void => {
setDescription(event.target.value);
}}
/>
</FormField>
<div>
<div style={{ marginBlockEnd: "var(--space-sm)", color: "var(--text-dim)" }}>Try an example</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: "var(--space-sm)" }}>
{EXAMPLES.map(
(example: string): ReactElement => (
<Button
key={example}
size="sm"
variant="ghost"
textCase="none"
onClick={(): void => {
setDescription(example);
}}
>
{example}
</Button>
),
)}
</div>
</div>
</WizardStep>
);
}
A wizard step is the panel of one question in a flow: a title with a close button, a line saying what to do, the step's
content (choice cards, fields, examples to try), and a footer. The footer has a band of its own under a hairline: a note
at its start saying what will happen with the answer, and the step's buttons at its end, the primary action last. A flow
of several steps adds progress, a row of segments with the position in words ("Step 2 of 3"), above the title.
It is a named section, not a dialog. Put it inside a modal for an overlay, on a page for a screen, or let it fill a phone.
The caller translates every word, so the component adds no catalog keys, and onClose shows the close button only when
the caller also gives it a name.
A step holds no state of its own: the flow around it decides which step shows and keeps the answers. A schema form laid out as a wizard keeps a new record's draft with the step the crew reached, so a flow closed halfway opens again on that step, saying so, with Start over to begin again from the first.
Examples
With progress
When is it due?
Bosun reminds you a week before.
Code
import { Button } from "@bosunkit/components/button";
import { FormField } from "@bosunkit/components/form-field";
import { Input } from "@bosunkit/components/input";
import { WizardStep } from "@bosunkit/components/wizard-step";
import type { ReactElement } from "react";
export const description: string =
"A step in a flow of three: the segments above the title and the words \"Step 2 of 3\" say where the crew are, with Back " +
"and Next at the end of the footer.";
export default function WithProgress(): ReactElement {
return (
<WizardStep
title="When is it due?"
description="Bosun reminds you a week before."
progress={{ current: 2, total: 3, label: "Step 2 of 3" }}
note="Engine hours come from the last reading"
actions={
<>
<Button variant="ghost">Back</Button>
<Button variant="primary">Next</Button>
</>
}
>
<FormField label="Every how many engine hours?">
<Input aria-label="Every how many engine hours?" inputMode="numeric" defaultValue="200" />
</FormField>
</WizardStep>
);
}
The explanation and the navigation
Choose the vessel
The checklist applies to this vessel.
The vessel's choices.
Code
import { WizardStep } from "@bosunkit/components/wizard-step";
import type { ReactElement } from "react";
export const description: string =
"A step with the why beside the what: the explanation says what the answer decides, the Back and Next buttons carry the whole phrases.";
export default function Navigated(): ReactElement {
return (
<WizardStep
title="Choose the vessel"
description="The checklist applies to this vessel."
progress={{ current: 2, total: 3, label: "Step 2 of 3" }}
explanation="The vessel decides the checklist's scope: its equipment, its crew and its routes."
note="Nothing is saved until Finish."
previousLabel="Back to the record"
onPrevious={(): void => {}}
nextLabel="Next: the checklist"
onNext={(): void => {}}
>
<p>The vessel's choices.</p>
</WizardStep>
);
}
explanation says why the flow asks this and what the answer decides, as a quiet inset the crew can read before
answering. Back and the primary action draw only when the caller gives each its whole phrase and its handler, so a
first step has no dead end and a step that cannot continue has no primary. While the caller's work after the primary
is under way it sets pending: the button disables and shows the spinner, and nothing advances until it clears.
The last page
The record is on the schedule
The crew are notified.
- Vessel
- Althea
- Work
- Anchor windlass service
- Due
- 14 October
Code
import { WizardFinal } from "@bosunkit/components/wizard-step";
import type { ReactElement } from "react";
export const description: string =
"The wizard's last page: the flow finished, the answers to check, and the one done action.";
export default function Finished(): ReactElement {
return (
<WizardFinal
title="The record is on the schedule"
description="The crew are notified."
summary={[
{ label: "Vessel", value: "Althea" },
{ label: "Work", value: "Anchor windlass service" },
{ label: "Due", value: "14 October" },
]}
doneLabel="Done"
onDone={(): void => {}}
/>
);
}
WizardFinal is the wizard's last page: the flow finished, the answers the crew check before they leave, and the
one done action. The mark is the message success color, never an alert color — finishing is not an alarm.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| titlerequired | string | – | What the step asks for ("Add a maintenance record"), translated by the caller. |
| description | string | undefined | – | One line under the title saying what to do or what happens next, translated by the caller. |
| progress | WizardProgress | undefined | – | Where the step sits in its flow; a row of segments and its label above the title. Omit it for a one-step panel. |
| childrenrequired | ReactNode | – | The step's content: choice cards, fields, suggestions. |
| explanation | ReactNode | undefined | – | The step's explanation, shown as an inset at the top of the content: why the flow asks this, and what the answer decides. The caller's words, translated. |
| previousLabel | string | undefined | – | The step before this one, as the Back button's whole phrase ("Back to the vessel"), translated by the caller.
The button draws only when both this and onPrevious are set, so a first step has no dead end. |
| onPrevious | (() => void) | undefined | – | Called when the crew press Back; their answers stay with the caller, which the Back button never discards. |
| nextLabel | string | undefined | – | The primary action's whole phrase, translated by the caller and named by what happens ("Next: the engine",
"Finish"). The button draws only when both this and onNext are set. |
| onNext | (() => void) | undefined | – | Called when the crew press the primary action. |
| pending | boolean | undefined | false | Whether the step the crew asked for is still being done, which the caller sets and clears around its own work after
onNext; while it is set the button is disabled and shows a spinner. |
| note | ReactNode | undefined | – | A line at the start of the footer saying what will happen ("Saved as a draft, not yet on the schedule"). |
| actions | ReactNode | undefined | – | The step's own buttons at the end of the footer, before the primary action; none when the caller has none. |
| closeLabel | string | undefined | – | The name of the close button ("Close"), translated by the caller. The button shows only when onClose is set. |
| onClose | (() => void) | undefined | – | Called when the crew press the close button. |
| headingLevel | 1 | 2 | 3 | 4 | undefined | 2 | The heading level of the title, 2 by default. |
| className | string | undefined | – | Added to the panel's classes. |
| testId | string | undefined | – | Becomes data-testid on the panel. |
| ref | Ref<HTMLElement> | undefined | – | The panel element. |
| Prop | Type | Default | Description |
|---|---|---|---|
| titlerequired | string | – | What finished ("The record is on the schedule"), translated by the caller. |
| description | string | undefined | – | A line under the title saying what happens next, translated by the caller. |
| summary | readonly WizardSummaryRow[] | undefined | – | The answers the crew check before they leave, label and value each. |
| doneLabelrequired | string | – | The done button's phrase ("Done"), translated by the caller. |
| onDonerequired | () => void | – | Called when the crew press done. |
| closeLabel | string | undefined | – | The close button's name ("Close"), translated by the caller; the button shows only when onClose is set. |
| onClose | (() => void) | undefined | – | Called when the crew press the close button. |
| headingLevel | 1 | 2 | 3 | 4 | undefined | 2 | The heading level of the title, 2 by default. |
| className | string | undefined | – | Added to the panel's classes. |
| testId | string | undefined | – | Becomes data-testid on the panel. |
| ref | Ref<HTMLElement> | undefined | – | The panel element. |
Add a record
Add a record
Related
-
Steps: name the stages, show explicit completion and revisit available stages.
-
Radio cards: the cards a step uses to ask how to start.
-
Task dialog: the dialog a step usually opens in.
-
Form field: the label, hint and error of a field in a step.