# Breadcrumb Steps Progress — React Breadcrumb Component

> A breadcrumb of interlocking arrow segments for linear flows: colour sweeps into the next arrow as you advance and finished steps swap their number for a check.

- Category: [Breadcrumbs](https://pixelpapa.com/components/breadcrumbs.md)
- Tags: breadcrumb, steps, progress, checkout, arrows
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/breadcrumbs/breadcrumb-steps-progress

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/breadcrumb-steps-progress.json"
```

The file is written to `components/pixelpapa/breadcrumb-steps-progress.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { Check } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useState } from "react";
import { cn } from "@/lib/utils";

type StepsBreadcrumbProps = {
  steps: string[];
  current: number;
  /** Lets people jump back to completed steps. */
  onStepChange?: (index: number) => void;
  className?: string;
};

const TIP = 14; // px depth of the arrow point

function shape(first: boolean, last: boolean) {
  const right = last ? "100% 0, 100% 100%" : `calc(100% - ${TIP}px) 0, 100% 50%, calc(100% - ${TIP}px) 100%`;
  return `polygon(0 0, ${right}, 0 100%${first ? "" : `, ${TIP}px 50%`})`;
}

export function StepsBreadcrumb({ steps, current, onStepChange, className }: StepsBreadcrumbProps) {
  const reduce = useReducedMotion();

  return (
    <nav aria-label="Progress" className={className}>
      <ol className="flex overflow-hidden rounded-xl">
        {steps.map((label, i) => {
          const done = i < current;
          const active = i === current;
          return (
            <li
              key={label}
              style={{ clipPath: shape(i === 0, i === steps.length - 1), marginLeft: i === 0 ? 0 : -(TIP - 4) }}
              className="relative min-w-0 flex-1"
            >
              <button
                type="button"
                disabled={!done || !onStepChange}
                aria-current={active ? "step" : undefined}
                onClick={() => onStepChange?.(i)}
                className={cn(
                  "group relative flex h-11 w-full items-center justify-center gap-2 bg-muted pr-4 text-sm outline-none",
                  i === 0 ? "pl-4" : "pl-7",
                  done && onStepChange && "cursor-pointer",
                )}
              >
                <motion.span
                  aria-hidden
                  initial={false}
                  animate={{ scaleX: done || active ? 1 : 0 }}
                  transition={{ duration: reduce ? 0 : 0.45, ease: [0.65, 0, 0.35, 1] }}
                  className={cn(
                    "absolute inset-0 origin-left transition-colors duration-500 group-focus-visible:brightness-110",
                    active ? "bg-primary" : "bg-primary/15 group-hover:bg-primary/25",
                  )}
                />
                <span
                  className={cn(
                    "relative flex size-5 shrink-0 items-center justify-center rounded-full text-[11px] font-semibold transition-colors duration-300",
                    active && "bg-primary-foreground text-primary",
                    done && "bg-primary text-primary-foreground",
                    !active && !done && "bg-muted-foreground/20 text-muted-foreground",
                  )}
                >
                  <AnimatePresence mode="popLayout" initial={false}>
                    <motion.span key={done ? "check" : "n"} initial={{ scale: 0, rotate: -60 }} animate={{ scale: 1, rotate: 0 }} exit={{ scale: 0 }} transition={{ type: "spring", stiffness: 500, damping: 26 }}>
                      {done ? <Check className="size-3" strokeWidth={3.5} /> : i + 1}
                    </motion.span>
                  </AnimatePresence>
                </span>
                <span
                  className={cn(
                    "relative truncate transition-colors duration-300 group-focus-visible:underline",
                    active ? "font-semibold text-primary-foreground" : done ? "font-medium text-primary" : "text-muted-foreground",
                  )}
                >
                  {label}
                </span>
              </button>
            </li>
          );
        })}
      </ol>
    </nav>
  );
}

const steps = ["Cart", "Shipping", "Payment", "Review"];
const copy = [
  "3 items · $184.00. Free returns within 30 days.",
  "Standard delivery to Utrecht arrives Thursday, Nov 14.",
  "Card, iDEAL or pay in three instalments.",
  "Check the details once more, then place the order.",
];

export default function BreadcrumbStepsProgressDemo() {
  const [current, setCurrent] = useState(1);
  const btn = "h-9 rounded-full px-4 text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring/60 disabled:pointer-events-none disabled:opacity-40";

  return (
    <div className="w-full max-w-xl rounded-2xl border border-border bg-card p-5 shadow-sm">
      <StepsBreadcrumb steps={steps} current={current} onStepChange={setCurrent} />
      <div className="mt-5 flex items-center justify-between gap-4">
        <div aria-live="polite" className="min-w-0">
          <p className="text-sm font-semibold text-card-foreground">
            Step {current + 1} of {steps.length} · {steps[current]}
          </p>
          <p className="mt-0.5 truncate text-sm text-muted-foreground">{copy[current]}</p>
        </div>
        <div className="flex shrink-0 gap-1.5">
          <button type="button" disabled={current === 0} onClick={() => setCurrent(current - 1)} className={cn(btn, "text-muted-foreground hover:bg-muted hover:text-foreground")}>
            Back
          </button>
          <button
            type="button"
            onClick={() => setCurrent(current === steps.length - 1 ? 0 : current + 1)}
            className={cn(btn, "bg-primary text-primary-foreground shadow-sm shadow-primary/30 hover:brightness-110")}
          >
            {current === steps.length - 1 ? "Start over" : "Continue"}
          </button>
        </div>
      </div>
    </div>
  );
}
```

## AI prompt

```text
Add the "Breadcrumb Steps Progress" component from PixelPapa UI to this project.

What it is: A breadcrumb of interlocking arrow segments for linear flows: colour sweeps into the next arrow as you advance and finished steps swap their number for a check.
Category: Breadcrumbs. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/breadcrumb-steps-progress.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/breadcrumb-steps-progress.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/breadcrumb-steps-progress" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More breadcrumbs

- [Breadcrumb Collapsed Dropdown](https://pixelpapa.com/components/breadcrumbs/breadcrumb-collapsed-dropdown.md): A breadcrumb that folds its middle levels into an ellipsis button; the menu springs open and lists the hidden levels as an indented staircase.
- [Breadcrumb Icons Chevrons](https://pixelpapa.com/components/breadcrumbs/breadcrumb-icons-chevrons.md): An icon breadcrumb that assembles itself on mount, each crumb sliding out from behind its chevron, with a hover highlight that glides between items.
- [Breadcrumb Path Editable](https://pixelpapa.com/components/breadcrumbs/breadcrumb-path-editable.md) (Pro): A file-explorer breadcrumb: click the bar and the crumbs turn into a typed path; Enter navigates, a path that does not exist shakes, Escape restores the crumbs.
