Steps
A stepper for a multi-stage flow, with per-step content and navigation.
Usage
import {
Steps,
StepsCompletedContent,
StepsContent,
StepsDescription,
StepsIndicator,
StepsItem,
StepsList,
StepsNext,
StepsPrevious,
StepsSeparator,
StepsTitle,
StepsTrigger,
} from "@kanzo-tech/ui";<Steps count={3} defaultStep={0}>
<StepsList>
<StepsItem index={0}>
<StepsTrigger>
<StepsIndicator>1</StepsIndicator>
<StepsTitle>Connect</StepsTitle>
</StepsTrigger>
<StepsSeparator />
</StepsItem>
</StepsList>
<StepsContent index={0}>…</StepsContent>
</Steps>count is required — the machine needs to know the total to decide when the flow is
complete. StepsItem and StepsContent are matched by index.
Anatomy
<Steps count>
<StepsList>
<StepsItem index>
<StepsTrigger>
<StepsIndicator /> ← number, swapped for a check once complete
<StepsTitle />
<StepsDescription />
<StepsSeparator /> ← hidden on the last item
<StepsContent index />
<StepsCompletedContent /> ← shown after the final step
<StepsPrevious /> <StepsNext />StepsIndicator renders its children and a CheckIcon, showing whichever the step's state
calls for — you always pass the number, never the tick.
Vertical
StepsSeparator absolutely positions itself down the indicator column in vertical mode, so
the connecting line follows the list without any extra markup.
Accessibility
The triggers announce themselves as tabs and no arrow key moves between them. Ark's machine
emits role="tablist" and role="tab" with the aria-selected / aria-controls wiring, and
@zag-js/steps@1.41.2 ships no key handler at all — ← → ↑
↓ Home End all do nothing. @zag-js/tabs, the same version by the
same team, implements all six.
So a screen-reader user is told this is a tab list, tries the keys that a tab list answers, and
gets nothing. What works today is Tab and Shift+Tab: with the
default linear={false} every trigger is its own tab stop, so the list is reachable and operable
— just not the way the role advertises.
| Key | What happens |
|---|---|
| Tab / Shift+Tab | Moves to the next / previous trigger. Every trigger is a stop. |
| Enter / Space | Activates the focused trigger, as a button. |
| ← → ↑ ↓ Home End | Nothing. |
linear={true} is not the fix and is a worse state: it drops the non-current triggers to
tabIndex: -1 and makes their click handler return early, so a step becomes unreachable by
pointer and keyboard alike while still announcing role="tab". The reasoning is in
the references,
and packages/ui/src/simples/steps.test.ts
holds the measurement — it fails on the day Ark ships the keymap, which is when this section
should be rewritten.
If a keyboard-complete stepper is a requirement today, drive Tabs with
step-shaped styling instead; it is the same role with the keymap behind it.
API Reference
All props are Ark's Steps.Root; the components add styling and the indicator's
complete-state swap only.
| Prop | Type | Default |
|---|---|---|
count | number | — |
step / defaultStep | number | 0 |
onStepChange | (details: { step: number }) => void | — |
orientation | "horizontal" | "vertical" | "horizontal" |
linear | boolean | false |
useSteps re-exports Ark's useStepsContext for reading the current step from a descendant.