Steps

nav.steps

Steps — the trail through a multi-step flow: checkout, a wizard, onboarding.

Examples

A checkout, mid-flow

View “A checkout, mid-flow” as
<nav class="steps" aria-label="Checkout progress">
  <ol class="steps-list" role="list">
    <li class="steps-item" data-state="complete">
      <a class="steps-link" href="#cart">
        <span class="steps-marker" aria-hidden="true">1</span>
        <span class="steps-text">
          <span class="steps-label">Cart <span class="sr-only">(completed)</span></span>
          <span class="steps-desc">3 items</span>
        </span>
      </a>
    </li>
    <li class="steps-item" aria-current="step">
      <a class="steps-link" href="#delivery">
        <span class="steps-marker" aria-hidden="true">2</span>
        <span class="steps-text">
          <span class="steps-label">Delivery</span>
          <span class="steps-desc">Address and times</span>
        </span>
      </a>
    </li>
    <li class="steps-item" data-state="upcoming">
      <span class="steps-content">
        <span class="steps-marker" aria-hidden="true">3</span>
        <span class="steps-text">
          <span class="steps-label">Payment <span class="sr-only">(upcoming)</span></span>
          <span class="steps-desc">Card or transfer</span>
        </span>
      </span>
    </li>
  </ol>
</nav>

Current is aria-current="step" because the platform models it; complete and upcoming are data-state because it does not. The tick and the fill are pixels — the sr-only words are what a screen reader gets.

Stacked, for a narrow screen

View “Stacked, for a narrow screen” as
<nav class="steps steps-vertical" aria-label="Setup progress">
  <ol class="steps-list" role="list">
    <li class="steps-item" data-state="complete">
      <span class="steps-content">
        <span class="steps-marker" aria-hidden="true">1</span>
        <span class="steps-text">
          <span class="steps-label">Create account <span class="sr-only">(completed)</span></span>
        </span>
      </span>
    </li>
    <li class="steps-item" aria-current="step">
      <span class="steps-content">
        <span class="steps-marker" aria-hidden="true">2</span>
        <span class="steps-text">
          <span class="steps-label">Verify email</span>
        </span>
      </span>
    </li>
  </ol>
</nav>

A step that cannot be reached yet is a <span class="steps-content">, not an anchor. A disabled-looking link that still navigates is worse than no link.

Tokens 29

Level 2, declared on .steps itself. Set any of them on that selector to restyle this component without touching the skin.

Level 2 tokens declared by steps
TokenDefault
--steps-connector-colorvar(--color-border)
--steps-connector-insetvar(--space-2)
--steps-connector-size2px
--steps-desc-colorvar(--color-on-surface-subtle)
--steps-desc-font-sizevar(--font-size-xs)
--steps-label-colorvar(--color-on-surface-muted)
--steps-label-font-weightvar(--font-weight-medium)
--steps-marker-bgtransparent
--steps-marker-border-colorvar(--color-border-strong)
--steps-marker-border-widthvar(--border-width)
--steps-marker-colorvar(--color-on-surface-muted)
--steps-marker-font-sizevar(--font-size-sm)
--steps-marker-radiusvar(--radius-full)
--steps-marker-size2rem
--steps-complete-bgvar(--color-success)
--steps-complete-colorvar(--color-on-success)
--steps-complete-connector-colorvar(--color-success)
--steps-complete-label-colorvar(--color-on-surface)
--steps-current-bgvar(--color-primary)
--steps-current-colorvar(--color-on-primary)
--steps-current-label-colorvar(--color-on-surface)
--steps-current-label-font-weightvar(--font-weight-semibold)
--steps-font-familyvar(--font-secondary)
--steps-font-sizevar(--font-size-sm)
--steps-gapvar(--space-2)
--steps-item-gapvar(--space-2)
--steps-text-gapvar(--space-1)
--steps-markurl("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E")
--steps-mark-size55%

Variants and states

Variants

  • .steps-lg
  • .steps-sm

Inside it

  • .steps-item
  • .steps-vertical
  • .steps-marker
  • .steps-label
  • .steps-link
  • .steps-list
  • .steps-content
  • .steps-text
  • .steps-desc

State it reads

Read from the platform, never mirrored into a class that could disagree with it.

  • :focus
  • :hover
  • data-state

Before you ship it

What you have to do 6 requirements

  1. Put aria-current="step" on exactly one step, and only on the step the user is on. It may sit on the <li> (as above) or on the link inside it; this file styles either.
  2. Name the landmark — aria-label="Checkout progress" on the <nav> — when there is a <nav>. CSS cannot supply a name.
  3. Keep role="list" on the <ol>. The reset strips markers from a classed list, and that is what makes VoiceOver stop announcing "list, 3 items".
  4. Say "completed" and "upcoming" in text. The tick and the fill are pixels; they say nothing to a screen reader. A <span class="sr-only"> inside the label is the whole fix, and only aria-current comes free.
  5. Make upcoming steps a <span class="steps-content"> rather than a link when they cannot be reached yet. A disabled-looking anchor that still navigates is worse than no anchor.
  6. Put the class on the anchor itself. .steps-item a { … } leaves the anchor classless and the reset hands it back the UA underline. .steps-list, .steps-item, .steps-link, .steps-content, .steps-marker, .steps-text, .steps-label and .steps-desc are only meaningful inside .steps; every property that reads a component token carries a fallback, so one used loose degrades to the page defaults instead of to an undefined variable.

src/css/components/steps.css · npx mostlycss add steps