# Steps

Progress through a multi-step flow: connectors fill, finished steps draw a check, and the current one pulses ember.

**Category:** Navigation  
**Exports:** `Steps`  
**npm dependencies:** motion (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/steps.json
```
The file lands at `components/ui/steps.tsx`; import from `@/components/ui/steps`.

## API — Steps
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `steps` | `{ title: string; description?: string }[]` | — |  |
| `current` | `number` | — | Zero-based. |

## Example — Steps

```tsx
import * as React from "react";
import { Steps } from "@/components/ui/steps";
import { Button } from "@/components/ui/button";

const steps = [
  { title: "Account", description: "Email and password" },
  { title: "Workspace", description: "Name your team" },
  { title: "Invite", description: "Add teammates" },
  { title: "Done", description: "Start building" },
];

export default function StepsDemo() {
  const [current, setCurrent] = React.useState(1);
  return (
    <div className="grid w-full max-w-xl gap-8">
      <Steps steps={steps} current={current} />
      <div className="flex justify-center gap-2">
        <Button variant="ghost" onClick={() => setCurrent((c) => Math.max(0, c - 1))} disabled={current === 0}>
          Back
        </Button>
        <Button onClick={() => setCurrent((c) => Math.min(steps.length - 1, c + 1))} disabled={current === steps.length - 1}>
          Continue
        </Button>
      </div>
    </div>
  );
}
```

## Source — components/ui/steps.tsx

```tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export type Step = { title: string; description?: string };

export type StepsProps = {
  steps: Step[];
  /** Zero-based index of the current step; steps before it are complete. */
  current: number;
  className?: string;
};

export function Steps({ steps, current, className }: StepsProps) {
  const reduce = useReducedMotion();
  return (
    <ol className={cn("flex w-full items-start", className)} aria-label="Progress">
      {steps.map((step, i) => {
        const done = i < current;
        const active = i === current;
        return (
          <li key={step.title} className="relative flex flex-1 flex-col items-center text-center" aria-current={active ? "step" : undefined}>
            {i > 0 && (
              <span aria-hidden className="absolute right-1/2 top-4 h-0.5 w-[calc(100%-2.5rem)] -translate-x-5 overflow-hidden rounded-full bg-muted">
                <motion.span
                  className="block h-full origin-left bg-foreground"
                  initial={false}
                  animate={{ scaleX: i <= current ? 1 : 0 }}
                  transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 160, damping: 26 }}
                />
              </span>
            )}
            <motion.span
              className={cn(
                "relative z-10 grid size-8 place-items-center rounded-full border-2 text-xs font-semibold tabular-nums transition-colors duration-300",
                done ? "border-foreground bg-foreground text-background" : active ? "border-foreground bg-background text-foreground" : "border-input bg-background text-muted-foreground",
              )}
              animate={active && !reduce ? { scale: [1, 1.08, 1] } : { scale: 1 }}
              transition={{ duration: 0.4 }}
            >
              {active && !reduce && (
                <motion.span
                  className="absolute -inset-1 rounded-full border-2 border-ember"
                  animate={{ scale: [1, 1.35], opacity: [0.6, 0] }}
                  transition={{ repeat: Infinity, duration: 1.6, ease: "easeOut" }}
                  aria-hidden
                />
              )}
              {done ? (
                <svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
                  <motion.path d="M5 12.5l4.5 4.5L19 7" initial={{ pathLength: reduce ? 1 : 0 }} animate={{ pathLength: 1 }} transition={{ duration: 0.3, delay: 0.15 }} />
                </svg>
              ) : (
                i + 1
              )}
            </motion.span>
            <span className={cn("mt-2 text-sm font-medium transition-colors", i <= current ? "text-foreground" : "text-muted-foreground")}>{step.title}</span>
            {step.description && <span className="mt-0.5 hidden text-xs text-muted-foreground sm:block">{step.description}</span>}
            <span className="sr-only">{done ? "(complete)" : active ? "(current)" : "(not started)"}</span>
          </li>
        );
      })}
    </ol>
  );
}
```