Stepper

Shows how far through a sequential task the user is and how much remains. Use it for

Navigation Stepper.jsx Stepper.d.ts Stepper.md

Playground

Change a prop and watch it. The code below follows along.

Install and import

Stepper is in @dovetail-ds/react. Install it with React, if you haven't:

npm install @dovetail-ds/react react react-dom
import { Stepper } from "@dovetail-ds/react";

Import @dovetail-ds/react/styles.css once at your app's root, then your theme after it. Setup covers the stylesheets, fonts and dark mode.

Guidelines

Shows how far through a sequential task the user is and how much remains. Use it for checkout, onboarding, and setup wizards.

Rules

  • Steps must be genuinely sequential. If the user can complete them in any order, use Tabs or a checklist.
  • Three to five steps. A seven-step stepper reads as a warning, not a guide.
  • Step labels are nouns or short verb phrases, sentence case: "Shipping address", not "STEP 2: ENTER YOUR SHIPPING ADDRESS".
  • The active step carries aria-current="step". Do not also mark it with colour alone.
  • Use vertical when step descriptions run longer than a few words.

Tradeoffs

Showing the full path sets expectations but can deter users at step one. If your flow is long, consider splitting it so the first commitment is small.

Props

The typed contract, from Stepper.d.ts.

import * as React from "react";

export interface Step {
  id?: string;
  label: React.ReactNode;
  description?: React.ReactNode;
}

/** Shows position in a linear, multi-step task. */
export interface StepperProps extends React.HTMLAttributes<HTMLElement> {
  steps: Step[];
  /** Index of the active step, 0-based. Earlier steps render as complete. */
  current: number;
  /** @default "horizontal" */
  orientation?: "horizontal" | "vertical";
  /** @default "Progress" */
  label?: string;
}

export declare function Stepper(props: StepperProps): React.JSX.Element;

Tokens it reads

Every custom property Stepper.jsx resolves, read from the source: 20 semantic, 3 primitive. Component tier first, because that is the one to reach for.

  • semantic Shared roles. Override one and everything using that role moves with it.
  • primitive Raw values. A component reading one directly is a deliberate exception.
TokenTierDeclared as
--dt-border-defaultsemanticvar(--dt-color-neutral-200)
--dt-border-subtlesemanticvar(--dt-color-neutral-100)
--dt-border-width-defaultsemanticvar(--dt-dim-hair)
--dt-radius-pillsemanticvar(--dt-radius-raw-full)
--dt-space-inline-smsemanticvar(--dt-dim-3)
--dt-space-inline-xssemanticvar(--dt-dim-2)
--dt-space-stack-2xssemanticvar(--dt-dim-1)
--dt-space-stack-smsemanticvar(--dt-dim-3)
--dt-surface-actionsemanticvar(--dt-color-neutral-900)
--dt-surface-sunkensemanticvar(--dt-color-neutral-100)
--dt-text-body-xs-familysemanticvar(--dt-font-family-sans)
--dt-text-body-xs-sizesemanticvar(--dt-font-size-xs)
--dt-text-label-md-familysemanticvar(--dt-font-family-secondary)
--dt-text-label-md-sizesemanticvar(--dt-font-size-sm)
--dt-text-label-sm-familysemanticvar(--dt-font-family-secondary)
--dt-text-label-sm-sizesemanticvar(--dt-font-size-xs)
--dt-text-on-actionsemanticvar(--dt-color-white)
--dt-text-primarysemanticvar(--dt-color-neutral-900)
--dt-text-secondarysemanticvar(--dt-color-neutral-600)
--dt-text-tertiarysemanticvar(--dt-color-neutral-500)
--dt-dim-hairprimitive1px
--dt-font-weight-mediumprimitive500
--dt-font-weight-semiboldprimitive600

Source

React only, no dependencies, every value a token.

import React from "react";

export function Stepper({ steps = [], current = 0, orientation = "horizontal", label = "Progress", style, ...rest }) {
  const horiz = orientation === "horizontal";
  return (
    <nav aria-label={label} style={style} {...rest}>
      <ol style={{ listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: horiz ? "row" : "column", gap: horiz ? "var(--dt-space-inline-sm)" : "var(--dt-space-stack-sm)" }}>
        {steps.map((s, i) => {
          const done = i < current, active = i === current;
          const ring = done ? "var(--dt-surface-action)" : active ? "var(--dt-surface-action)" : "var(--dt-surface-sunken)";
          const fg = done || active ? "var(--dt-text-on-action)" : "var(--dt-text-tertiary)";
          return (
            <li key={s.id || i} aria-current={active ? "step" : undefined} style={{ display: "flex", flexDirection: horiz ? "column" : "row", gap: horiz ? "var(--dt-space-stack-2xs)" : "var(--dt-space-inline-sm)", flex: horiz ? 1 : "none", alignItems: horiz ? "stretch" : "flex-start", minWidth: 0 }}>
              <span style={{ display: "flex", alignItems: "center", gap: "var(--dt-space-inline-xs)" }}>
                <span aria-hidden="true" style={{
                  width: 24, height: 24, flex: "none", borderRadius: "var(--dt-radius-pill)",
                  background: ring, color: fg, display: "inline-flex", alignItems: "center", justifyContent: "center",
                  fontFamily: "var(--dt-text-label-sm-family)", fontSize: "var(--dt-text-label-sm-size)",
                  fontWeight: "var(--dt-font-weight-medium)",
                  border: active ? "none" : done ? "none" : "var(--dt-border-width-default) solid var(--dt-border-default)",
                }}>{done ? "✓" : i + 1}</span>
                {horiz && i < steps.length - 1 && <span aria-hidden="true" style={{ flex: 1, height: "var(--dt-dim-hair)", background: done ? "var(--dt-surface-action)" : "var(--dt-border-subtle)" }} />}
              </span>
              <span style={{ minWidth: 0 }}>
                <span style={{
                  display: "block", fontFamily: "var(--dt-text-label-md-family)", fontSize: "var(--dt-text-label-md-size)",
                  fontWeight: active ? "var(--dt-font-weight-semibold)" : "var(--dt-font-weight-medium)",
                  color: active || done ? "var(--dt-text-primary)" : "var(--dt-text-secondary)",
                }}>{s.label}</span>
                {s.description && <span style={{ display: "block", fontFamily: "var(--dt-text-body-xs-family)", fontSize: "var(--dt-text-body-xs-size)", color: "var(--dt-text-tertiary)" }}>{s.description}</span>}
              </span>
            </li>
          );
        })}
      </ol>
    </nav>
  );
}

In context

Stepper also appears on the navigation reference cards, alongside the rest of its family.