Skip to content

Stepper component

components specs/components/stepper.kmd

A standalone step indicator: an ordered sequence of state circles joined by connector lines that shows a workflow's progress. Each step is completed (check), current (filled dot), upcoming (hollow outline), or error (bang). A linear stepper is a passive read-out; a non-linear one lets the user click a completed step to jump back. Horizontal or vertical, with an optional label per step. gov.br ships a standalone `step`; KDS had a wizard-multistep pattern but no reusable stepper component — this is it (reused by the wizard).

When this spec applies

Primary triggers

All triggers

Specification body

Stepper

R1 — Anatomy

A stepper is an ordered sequence of steps. Each step renders a state circle (Ø 28px) and an optional label (a caller-composed child — the component is font-free, so a label is a Text/child widget, never a glyph the stepper draws). Adjacent circles are joined by a connector line (length 32px, stroke 2px). The label sits below (horizontal) or beside (vertical) its circle, separated by one --kds-space-2 (8px) gap.

R2 — States

Each step is in exactly one state, painted from the active Palette:

StateCircleMarkToken
completedfilledcheck (✓)accent fill · accent-on mark
currentfilledcentered dot (0.16 × Ø)accent fill · accent-on dot
upcominghollow outlineborder/outline stroke
errorfilledbang (!)error fill · accent-on mark

The colours resolve through the DS's Palette (design-RFC-014), so a stepper re-themes and re-colours per active theme and design system with no per-widget override.

R3 — Orientation

horizontal (default) lays steps left→right, connectors horizontal; vertical lays them top→bottom, connectors vertical. Orientation drives the keyboard axis (R6).

R4 — Linear vs non-linear

  • linear (default): a passive progress read-out — no step is activatable.
  • non-linear: a completed step is clickable to jump back to it, firing on_step(index). Only completed steps are reachable; current, upcoming, and error steps are never activatable (a user cannot skip ahead past incomplete work). The current step is never a jump target of itself.

R5 — Accessibility semantics

The stepper exports a SemRole::Group carrying its accessible name, holding one node per step:

  • a reachable step (completed, in a non-linear stepper) is a SemRole::Button;
  • every other step is a SemRole::Label.

Each step node carries its state as its accessible value (completed/current/upcoming/error) so assistive tech announces aria-current on the current step. The roving-focus step (R6) is marked focused.

R6 — Keyboard (roving focus, one Tab stop)

Per navigation.kmd R10 / progress-indicator roving-focus shape:

  • Tab enters the stepper only if it has a reachable step; it is a single Tab stop (roving focus, not one stop per step).
  • The orientation's arrows (Left/Right horizontal, Up/Down vertical) move a roving cursor across the reachable steps and wrap.
  • Home/End jump the cursor to the first/last reachable step.
  • Enter/Space activates the cursor's step (fires on_step).

A fully-linear stepper (no reachable step) takes no Tab stop — it is a passive indicator, correctly skipped by keyboard navigation.

R7 — gov.br parity

Satisfies the gov.br step standalone component (govbr-ds-parity, GVB-25 / stack#400): the same completed/current/upcoming/error progression and horizontal/vertical layout. The KDS wizard pattern composes this stepper rather than re-implementing the indicator.

Não-escopo

  • The multi-step wizard flow (panel swapping, validation-gated advance) — that pattern composes the stepper; it is not this component.
  • Label typography — a label is a caller-composed child; its type comes from specs/fonts/typography.kmd, not this spec.
  • Numbered-vs-dot circle content beyond the R2 state marks — the state mark is the canonical indicator; a numbered variant, if added, is a follow-up that must keep the R2 states and R5 semantics.

Requirements (testable)

Requirement: State rendering {#req-stepper-state-rendering}

Each step SHALL render a 28px state circle in exactly one of completed, current, upcoming, or error, with the circle fill and mark resolved from the active Palette per the R2 state table.

Scenario: Completed step paints check on accent fill

  • GIVEN a stepper with a step whose state is completed
  • WHEN the stepper renders under the active Palette
  • THEN that step's circle is filled with the accent token
  • AND the step's mark is a check (✓) painted with the accent-on token

Scenario: Upcoming step is a hollow outline with no mark

  • GIVEN a stepper with a step whose state is upcoming
  • WHEN the stepper renders
  • THEN that step's circle is drawn as a hollow outline using the border/outline stroke
  • AND no mark glyph is painted inside the circle

Scenario: Error step paints a bang

  • GIVEN a stepper with a step whose state is error
  • WHEN the stepper renders under the active Palette
  • THEN that step's circle is filled with the error token
  • AND the step's mark is a bang (!) painted with the accent-on token

Requirement: Linear vs non-linear activation {#req-stepper-activation}

A linear stepper SHALL expose no activatable step; a non-linear stepper SHALL make only completed steps clickable to jump back, firing on_step(index), and SHALL NOT allow activating current, upcoming, or error steps.

Scenario: Clicking a completed step in a non-linear stepper jumps back

  • GIVEN a non-linear stepper with a completed step at index 1
  • WHEN the user clicks the step at index 1
  • THEN on_step(1) fires

Scenario: Upcoming step is never activatable

  • GIVEN a non-linear stepper with an upcoming step at index 3
  • WHEN the user clicks the step at index 3
  • THEN no on_step callback fires
  • AND the current step is unchanged

Scenario: Linear stepper activates nothing

  • GIVEN a linear stepper containing a completed step
  • WHEN the user clicks that completed step
  • THEN no on_step callback fires

Requirement: Accessibility semantics {#req-stepper-a11y-semantics}

The stepper SHALL export a SemRole::Group carrying its accessible name, exposing each reachable (non-linear completed) step as a SemRole::Button, every other step as a SemRole::Label, and each step node's accessible value SHALL equal its state so assistive tech announces aria-current on the current step.

Scenario: Reachable completed step is a button, others are labels

  • GIVEN a non-linear stepper with a completed step and an upcoming step
  • WHEN the accessibility tree is queried
  • THEN the root node has role SemRole::Group with the stepper's accessible name
  • AND the completed step node has role SemRole::Button
  • AND the upcoming step node has role SemRole::Label

Scenario: Current step exposes its state as accessible value

  • GIVEN a stepper whose step at index 2 is current
  • WHEN the accessibility node for index 2 is inspected
  • THEN its accessible value is current

Requirement: Roving-focus keyboard navigation {#req-stepper-keyboard}

The stepper SHALL be a single Tab stop with roving focus that is entered by Tab only when it has a reachable step; its orientation arrows SHALL move the cursor across reachable steps and wrap, Home/End SHALL jump to the first/last reachable step, and Enter/Space SHALL activate the cursor's step firing on_step.

Scenario: Arrow wraps across reachable steps

  • GIVEN a focused horizontal non-linear stepper whose cursor is on the last reachable step
  • WHEN the user presses Right arrow
  • THEN the roving cursor moves to the first reachable step

Scenario: Enter activates the cursor's step

  • GIVEN a focused non-linear stepper with the roving cursor on a reachable completed step at index 1
  • WHEN the user presses Enter
  • THEN on_step(1) fires

Scenario: Fully-linear stepper takes no Tab stop

  • GIVEN a fully-linear stepper with no reachable step
  • WHEN the user presses Tab to traverse the page
  • THEN focus skips the stepper and lands on the next focusable element

Requirement: Orientation drives layout and keyboard axis {#req-stepper-orientation}

A horizontal stepper SHALL lay steps left→right with horizontal connectors and use Left/Right arrows, and a vertical stepper SHALL lay steps top→bottom with vertical connectors and use Up/Down arrows.

Scenario: Vertical stepper responds to Down arrow

  • GIVEN a focused vertical non-linear stepper with the cursor on the first reachable step
  • WHEN the user presses Down arrow
  • THEN the roving cursor moves to the next reachable step downward

Scenario: Horizontal stepper ignores the cross-axis arrow

  • GIVEN a focused horizontal stepper with the cursor on a reachable step
  • WHEN the user presses Down arrow
  • THEN the roving cursor position is unchanged

References