# Steps (Caseroom component)

Progress through a flow: done, current (aria-current=step) and to come; horizontal when there is room. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

0.68% of the 2,062 sites in The UI Index have a stepper (14 sites).

## Install

- React: `npx shadcn@latest add https://www.themes.dev/caseroom/r/steps.json`
- Astro: `npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/steps.json`
- HTML: https://www.themes.dev/caseroom/r/html/steps.html (with https://www.themes.dev/caseroom/r/html/caseroom.css)

Page: https://www.themes.dev/caseroom/components/steps/ · Rules for agents: https://www.themes.dev/caseroom/AGENT.md

## Code

### steps.astro

```astro
---
import { cn } from '@/lib/utils';
import { CHECK_PATH, steps as c, type Step } from './steps.variants';

interface Props {
  steps: Step[];
  /** The step you are on, counted from 0. Steps before it are done. */
  current: number;
  label?: string;
  class?: string;
}
const { steps, current, label = 'Progress', class: className } = Astro.props;
---

<nav aria-label={label} class={cn(c.root, className)}>
  <ol class={c.list}>
    {
      steps.map((s, k) => {
        const state = k < current ? 'done' : k === current ? 'current' : 'todo';
        return (
          <li data-state={state} aria-current={state === 'current' ? 'step' : undefined} class={c.item}>
            <span class={c.rail}>
              <span class={c.mark}>
                {state === 'done' ? <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d={CHECK_PATH} /></svg> : k + 1}
              </span>
              <span class={c.line} aria-hidden="true"></span>
            </span>
            <span class={c.text}>
              <span class={c.title}>{s.title}<span class="sr-only">{state === 'done' ? ' (done)' : state === 'current' ? ' (current)' : ''}</span></span>
              {s.description && <span class={c.description}>{s.description}</span>}
            </span>
          </li>
        );
      })
    }
  </ol>
</nav>
```

### steps.tsx

```tsx
import { cn } from '@/lib/utils';
import { CHECK_PATH, steps as c, type Step } from './steps.variants';

export type { Step };

export interface StepsProps {
  steps: Step[];
  /** The step you are on, counted from 0. Steps before it are done. */
  current: number;
  label?: string;
  className?: string;
}

/** Progress through a flow: done, current (aria-current="step") and still to come. */
export function Steps({ steps, current, label = 'Progress', className }: StepsProps) {
  return (
    <nav aria-label={label} className={cn(c.root, className)}>
      <ol className={c.list}>
        {steps.map((s, k) => {
          const state = k < current ? 'done' : k === current ? 'current' : 'todo';
          return (
            <li key={k} data-state={state} aria-current={state === 'current' ? 'step' : undefined} className={c.item}>
              <span className={c.rail}>
                <span className={c.mark}>
                  {state === 'done' ? <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d={CHECK_PATH} /></svg> : k + 1}
                </span>
                <span className={c.line} aria-hidden="true"></span>
              </span>
              <span className={c.text}>
                <span className={c.title}>{s.title}<span className="sr-only">{state === 'done' ? ' (done)' : state === 'current' ? ' (current)' : ''}</span></span>
                {s.description && <span className={c.description}>{s.description}</span>}
              </span>
            </li>
          );
        })}
      </ol>
    </nav>
  );
}
```

### steps.variants.ts

```tsx
/** Progress through a flow: an ordered list of steps, done (a check), current (aria-current="step", ink) and still
 *  to come. A real sequence, so the numbers are earned. Horizontal from a wide container, vertical on phones. */
export const steps = {
  root: '@container w-full',
  list: 'm-0 grid list-none gap-4 p-0 @xl:flex @xl:items-start @xl:gap-0',
  item: 'group flex items-start gap-3 @xl:flex-1 @xl:flex-col @xl:gap-2',
  rail: 'flex items-center @xl:w-full',
  mark: 'grid size-7 shrink-0 place-items-center rounded-full border text-xs font-semibold tabular-nums transition-colors group-data-[state=done]:border-primary group-data-[state=done]:bg-primary group-data-[state=done]:text-primary-foreground group-data-[state=current]:border-2 group-data-[state=current]:border-primary group-data-[state=current]:text-foreground group-data-[state=todo]:border-input group-data-[state=todo]:text-muted-foreground [&_svg]:size-3.5',
  line: 'hidden h-px flex-1 bg-border group-data-[state=done]:bg-primary @xl:mx-2 @xl:block group-last:invisible',
  text: 'min-w-0 pt-0.5 @xl:pt-0 @xl:pr-4',
  title: 'block text-sm font-medium group-data-[state=todo]:text-muted-foreground',
  description: 'mt-0.5 block text-sm text-pretty text-muted-foreground',
};
export type Step = { title: string; description?: string };
export const CHECK_PATH = 'M3.5 8.5l3 3 6-7';
```
