Skip to content

Stepper

hc-stepper shows progress through a multi-step flow. It ships no JavaScript at all: the server renders the whole indicator on every step — including the marker content (the step number, or once a step completes) — so every state change arrives as markup. The indicator displays; navigation is the job of the step’s form (see the multi-step form recipe).

  1. Account (completed)
  2. Profile
  3. Review
StateSelectorEffect
Current[aria-current="step"]Accent marker (follows data-color), emphasized label.
Complete[data-state="complete"]Tinted marker; the server renders as the marker content.
Upcoming(no attribute)Muted marker and label.

The server renders the marker content. A number for pending steps, for completed ones — no CSS content tricks, no client mutation. Add a visually hidden (completed) suffix to completed labels so the state is announced; aria-current="step" covers the current one.

Render a completed step’s content as a link to its step URL if the flow allows revisiting — the stepper styles it quietly (underline on hover/focus). This is the server’s choice per step; the indicator never invents navigation. Keep the marker outside the link so the column layout holds:

  1. Account (completed)
  2. Profile
  3. Review

data-size="sm" shrinks the markers for dense layouts. Horizontal only — a vertical orientation is a deliberate non-goal for now.

  1. Account (completed)
  2. Profile
  3. Review

The server re-renders the whole indicator on every step: the step form’s response (see the multi-step form recipe) simply includes the <ol> again with aria-current moved. After completing step 1 the server returns:

<ol class="hc-stepper">
<li class="hc-stepper__step" data-state="complete">
<span class="hc-stepper__marker" aria-hidden="true"></span>
<span class="hc-stepper__label">Account
<span class="hc-sr-only">(completed)</span></span>
</li>
<li class="hc-stepper__step" aria-current="step">
<span class="hc-stepper__marker" aria-hidden="true">2</span>
<span class="hc-stepper__label">Profile</span>
</li>
<!-- …remaining steps unchanged (upcoming) -->
</ol>

No client mutation: the marker content ( / 2), data-state, and aria-current all arrive as markup.

  • An ordered list is the right bone structure: “list, 3 items” + aria-current="step" is the standard idiom.
  • Markers are aria-hidden (decorative duplicates of the position); the label text + sr-only suffixes carry the meaning.
  • RTL works for free (logical properties throughout, including the connector).

Component tokens (in component.tokens.json) — semantic references, so themes, accents and dark mode apply automatically:

Token pathPurpose
stepper.marker-sizeMarker diameter (data-size="sm" overrides it).
stepper.marker-bg / marker-fgUpcoming-step marker.
stepper.current-bg / current-fgCurrent-step marker (follows data-color).
stepper.complete-bg / complete-fgCompleted-step marker tint.
stepper.connectorConnector line between steps.
stepper.labelLabel color.
stepper.gapGap between marker and label.
Show the generated CSS variables
--hc-stepper-marker-size | -marker-bg | -marker-fg
--hc-stepper-current-bg | -current-fg
--hc-stepper-complete-bg | -complete-fg
--hc-stepper-connector | -label | -gap