Skip to content
design
Components

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.

Describe itBosun drafts the steps from plain words.From a templateTested schedules for your engine.Blank recordFill in every field yourself.
Try an example

Drafts are saved as not yet due

The first step of adding a maintenance record: how to start, what the job is, and what will happen.
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.

01Say what will happenThe footer note states what the answer does ("Drafts are saved as not yet due"), so the crew press the primary action knowing it.
02One primary action, lastCancel and Back come before the one primary action at the end of the footer, where the thumb ends.
03Say where they areIn a flow of several steps, the segments and the words "Step 2 of 3" both say how far along the crew are.

Examples

With progress

Step 2 of 3

When is it due?

Bosun reminds you a week before.

Engine hours come from the last reading

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.
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

Step 2 of 3

Choose the vessel

The checklist applies to this vessel.

The vessel's choices.

Nothing is saved until Finish.

A step with the why beside the what: the explanation says what the answer decides, the Back and Next buttons carry the whole phrases.
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
The wizard's last page: the flow finished, the answers to check, and the one done action.
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

PropTypeDefaultDescription
titlerequiredstring–What the step asks for ("Add a maintenance record"), translated by the caller.
descriptionstring | undefined–One line under the title saying what to do or what happens next, translated by the caller.
progressWizardProgress | 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.
childrenrequiredReactNode–The step's content: choice cards, fields, suggestions.
explanationReactNode | 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.
previousLabelstring | 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.
nextLabelstring | 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.
pendingboolean | undefinedfalseWhether 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.
noteReactNode | undefined–A line at the start of the footer saying what will happen ("Saved as a draft, not yet on the schedule").
actionsReactNode | undefined–The step's own buttons at the end of the footer, before the primary action; none when the caller has none.
closeLabelstring | 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.
headingLevel1 | 2 | 3 | 4 | undefined2The heading level of the title, 2 by default.
classNamestring | undefined–Added to the panel's classes.
testIdstring | undefined–Becomes data-testid on the panel.
refRef<HTMLElement> | undefined–The panel element.
PropTypeDefaultDescription
titlerequiredstring–What finished ("The record is on the schedule"), translated by the caller.
descriptionstring | undefined–A line under the title saying what happens next, translated by the caller.
summaryreadonly WizardSummaryRow[] | undefined–The answers the crew check before they leave, label and value each.
doneLabelrequiredstring–The done button's phrase ("Done"), translated by the caller.
onDonerequired() => void–Called when the crew press done.
closeLabelstring | 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.
headingLevel1 | 2 | 3 | 4 | undefined2The heading level of the title, 2 by default.
classNamestring | undefined–Added to the panel's classes.
testIdstring | undefined–Becomes data-testid on the panel.
refRef<HTMLElement> | undefined–The panel element.
Do

Add a record

Step content

Saved as a draft

Put the one primary action last in the footer, after Cancel or Back, with a note that says what it will do.
Don't

Add a record

Step content
Don't give a step two primary actions: the crew cannot tell which one goes on, and a wet thumb hits either.
  • 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.