Back to Machine Coding
Machine Coding
easy
mid

How would you build a stepper or progress tracker component?

List of steps with statuses (done | current | upcoming | error), driven by a `currentStep` prop or URL. Render an accessible `<ol>` with `aria-current='step'` on the active one. Allow click navigation to completed steps; lock forward navigation behind validation; show progress visually with line/circle states.

3 min read·~25 min to think through

A stepper communicates "where am I and what's left" in a multi-step flow. The component is small; the right abstraction matters.

1. API

jsx
<Stepper
  steps={[{ id: "account", label: "Account" }, { id: "profile", label: "Profile" }, ...]}
  current={currentStepId}
  completed={["account"]}                   // or derive
  errors={["profile"]}                      // optional
  onStepClick={(id) => goTo(id)}            // optional — gated
/>

2. Status per step

For each step compute one of: done | current | error | upcoming. That drives both styling and ARIA.

3. Markup — ordered list + aria-current

jsx
<ol className="stepper" aria-label="Progress">
  {steps.map((s, i) => {
    const status = getStatus(s, current, completed, errors);
    const isClickable = status === "done" || status === "current";
    return (
      <li
        key={s.id}
        className={`step step--${status}`}
        aria-current={status === "current" ? "step" : undefined}
      >
        {isClickable ? (
          <button type="button" onClick={() => onStepClick?.(s.id)}>
            <span className="step__index" aria-hidden="true">{i + 1}</span>
            <span className="step__label">{s.label}</span>
          </button>
        ) : (
          <>
            <span className="step__index" aria-hidden="true">{i + 1}</span>
            <span className="step__label">{s.label}</span>
          </>
        )}
      </li>
    );
  })}
</ol>

4. Navigation rules

  • Click on a completed step — allowed (back-navigation is free).
  • Click on the current step — no-op.
  • Click on an upcoming step — blocked or gated on validation of intervening steps.
  • Keyboard — Tab through the clickable steps; Enter/Space activate.

5. Visual states

  • Done — checkmark, filled.
  • Current — outlined, larger, focus ring.
  • Upcoming — muted.
  • Error — red, exclamation icon.
  • Line between steps reflects status (filled up to the current step).

6. Orientation

Horizontal on desktop, vertical on narrow viewports (CSS-only with @media or a prop).

7. Accessibility

  • <ol> for ordered semantics.
  • aria-current="step" on the active step (screen readers announce it).
  • Visible focus on clickable steps.
  • Don't rely on color alone — icons + text labels.
  • For dense steppers, label each step with its number and name; an aria-label on the container ("Checkout progress").

8. Driven by URL

The stepper should read currentStep from the URL so the back button and refresh work, and reflect it via aria-current. The parent owns the navigation logic; the stepper is a view.

Interview framing

"An ordered list (<ol>) with one item per step. Each step has a status (done/current/upcoming/error) computed from the current step id and the completed/errors sets. aria-current='step' on the active one. Click-to-navigate is allowed for completed steps; forward is gated on validation. Visuals follow status — checkmark for done, ring for current, muted for upcoming, red for error — never color-only. The stepper is a view; the parent owns navigation and validation."

Follow-up questions

  • Why use aria-current='step' instead of aria-selected?
  • How do you handle a step that's both completed and has errors (e.g., user edited an earlier step and broke validation)?
  • Horizontal vs vertical — when and how to swap?

Common mistakes

  • Color-only state indication.
  • No keyboard support on clickable steps.
  • Locking back-navigation behind validation.
  • Using <div> instead of <ol>/<li> — loses ordered semantics.

Performance considerations

  • Tiny component; performance not a concern. Make sure status derivation doesn't recompute heavy data — memoize if needed.

Edge cases

  • Many steps overflowing horizontally — collapse to 'Step 3 of 7' on mobile.
  • Step skipped optionally.
  • Step errored after being completed.

Real-world examples

  • Stripe Checkout, Shopify multi-step forms, account onboarding wizards.

Senior engineer discussion

Seniors keep the stepper a pure view driven by URL state, use `<ol>`+`aria-current`, allow free back-navigation, and treat clear status communication (icon + text + color) as accessibility, not decoration.

Related questions