# Stepper Horizontal Progress — React Stepper Component

> A horizontal stepper where the connector line fills toward the next step and each completed circle draws its checkmark.

- Category: [Steppers & Timelines](https://pixelpapa.com/components/steppers.md)
- Tags: stepper, progress, steps, checkmark, onboarding
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/steppers/stepper-horizontal-progress

## Install

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

The file is written to `components/pixelpapa/stepper-horizontal-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 { ArrowLeft, ArrowRight, RotateCcw } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useId, useState } from "react";
import { cn } from "@/lib/utils";

export type ProgressStep = { title: string; hint?: string };

type HorizontalStepperProps = {
  steps: ProgressStep[];
  /** Index of the active step. Pass `steps.length` when everything is complete. */
  current: number;
  /** Makes completed steps clickable. */
  onStepChange?: (index: number) => void;
  className?: string;
};

export function HorizontalStepper({ steps, current, onStepChange, className }: HorizontalStepperProps) {
  const uid = useId();
  return (
    <ol className={cn("flex w-full", className)}>
      {steps.map((step, i) => {
        const done = i < current;
        const active = i === current;
        const canJump = !!onStepChange && done;
        return (
          <li key={step.title} className="relative flex flex-1 flex-col items-center text-center">
            {i < steps.length - 1 && (
              <span aria-hidden className="absolute top-[17px] right-[calc(-50%+26px)] left-[calc(50%+26px)] h-0.5 overflow-hidden rounded-full bg-border">
                <motion.span
                  initial={false}
                  animate={{ scaleX: done ? 1 : 0 }}
                  transition={{ duration: 0.45, ease: [0.65, 0, 0.35, 1] }}
                  className="block h-full origin-left bg-primary"
                />
              </span>
            )}
            <button
              type="button"
              disabled={!canJump}
              aria-current={active ? "step" : undefined}
              aria-label={`Step ${i + 1}: ${step.title}${done ? " (completed)" : ""}`}
              onClick={() => onStepChange?.(i)}
              className={cn(
                "relative flex size-9 items-center justify-center rounded-full border-2 text-sm font-semibold transition-colors duration-300 outline-none focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-2 focus-visible:ring-offset-card",
                done && "border-primary bg-primary text-primary-foreground",
                active && "border-primary bg-card text-primary",
                !done && !active && "border-border bg-card text-muted-foreground",
                canJump && "cursor-pointer hover:brightness-110",
              )}
            >
              {active && (
                <motion.span
                  layoutId={`stepper-horizontal-progress-${uid}`}
                  className="absolute -inset-[7px] rounded-full bg-primary/15"
                  transition={{ type: "spring", stiffness: 300, damping: 30 }}
                />
              )}
              <AnimatePresence mode="wait" initial={false}>
                {done ? (
                  <motion.svg
                    key="check"
                    viewBox="0 0 16 16"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth={2.4}
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    className="relative size-4"
                    exit={{ opacity: 0, scale: 0.6 }}
                  >
                    <motion.path
                      d="M3.5 8.4l3 3 6-6.6"
                      initial={{ pathLength: 0 }}
                      animate={{ pathLength: 1 }}
                      transition={{ duration: 0.35, delay: 0.1, ease: "easeOut" }}
                    />
                  </motion.svg>
                ) : (
                  <motion.span
                    key="number"
                    initial={{ opacity: 0, scale: 0.6 }}
                    animate={{ opacity: 1, scale: 1 }}
                    exit={{ opacity: 0, scale: 0.6 }}
                    transition={{ duration: 0.15 }}
                    className="relative tabular-nums"
                  >
                    {i + 1}
                  </motion.span>
                )}
              </AnimatePresence>
            </button>
            <p className={cn("mt-3 text-sm font-medium transition-colors duration-300", done || active ? "text-foreground" : "text-muted-foreground")}>
              {step.title}
            </p>
            {step.hint && <p className="mt-0.5 hidden text-xs text-muted-foreground sm:block">{step.hint}</p>}
          </li>
        );
      })}
    </ol>
  );
}

const steps = [
  { title: "Account", hint: "Name and email", body: "Tell us who is setting things up. You can add co-owners later from Settings → Members." },
  { title: "Workspace", hint: "Region and URL", body: "Pick a workspace URL and the region your data lives in. Frankfurt and Oregon are available today." },
  { title: "Team", hint: "Invite people", body: "Paste a list of emails or share an invite link. New members join as editors unless you say otherwise." },
  { title: "Billing", hint: "Plan and card", body: "Start on the free plan or attach a card for Team. You are only charged for seats that sign in." },
  { title: "Launch", hint: "Review", body: "Everything looks good. Launching creates the workspace and sends the 4 pending invitations." },
];

export default function StepperHorizontalProgressDemo() {
  const [current, setCurrent] = useState(2);
  const finished = current >= steps.length;

  return (
    <div className="w-full max-w-2xl rounded-2xl border border-border bg-card p-6 shadow-sm sm:p-8">
      <HorizontalStepper steps={steps} current={current} onStepChange={setCurrent} />

      <div className="mt-8 min-h-[92px] rounded-xl bg-muted/50 p-5">
        <AnimatePresence mode="wait" initial={false}>
          <motion.div
            key={current}
            initial={{ opacity: 0, y: 8 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -8 }}
            transition={{ duration: 0.2 }}
          >
            <p className="text-sm font-semibold text-foreground">
              {finished ? "Workspace launched" : `Step ${current + 1} of ${steps.length} — ${steps[current].title}`}
            </p>
            <p className="mt-1.5 text-sm leading-relaxed text-muted-foreground">
              {finished ? "northwind.app/halden is live and your invitations are on their way." : steps[current].body}
            </p>
          </motion.div>
        </AnimatePresence>
      </div>

      <div className="mt-6 flex items-center justify-between">
        <button
          type="button"
          disabled={current === 0}
          onClick={() => setCurrent((c) => (finished ? 0 : c - 1))}
          className="inline-flex h-9 items-center gap-1.5 rounded-full px-3.5 text-sm font-medium text-muted-foreground transition-colors outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/60 disabled:pointer-events-none disabled:opacity-40"
        >
          {finished ? <RotateCcw className="size-4" /> : <ArrowLeft className="size-4" />}
          {finished ? "Start over" : "Back"}
        </button>
        {!finished && (
          <button
            type="button"
            onClick={() => setCurrent((c) => c + 1)}
            className="group inline-flex h-9 items-center gap-1.5 rounded-full bg-primary px-4 text-sm font-medium text-primary-foreground shadow-sm shadow-primary/30 transition-transform outline-none hover:-translate-y-px focus-visible:ring-2 focus-visible:ring-ring/60 focus-visible:ring-offset-2 focus-visible:ring-offset-card active:translate-y-0"
          >
            {current === steps.length - 1 ? "Launch workspace" : "Continue"}
            <ArrowRight className="size-4 transition-transform group-hover:translate-x-0.5" />
          </button>
        )}
      </div>
    </div>
  );
}
```

## AI prompt

```text
Add the "Stepper Horizontal Progress" component from PixelPapa UI to this project.

What it is: A horizontal stepper where the connector line fills toward the next step and each completed circle draws its checkmark.
Category: Steppers & Timelines. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/stepper-horizontal-progress.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/stepper-horizontal-progress.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/stepper-horizontal-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 steppers & timelines

- [Stepper Order Tracking](https://pixelpapa.com/components/steppers/stepper-order-tracking.md): A shipment tracker where a truck drives along a curved SVG route to the current stage, leaving a solid line behind it and lighting each stop it passes.
- [Stepper Vertical Details](https://pixelpapa.com/components/steppers/stepper-vertical-details.md) (Pro): A vertical stepper where the active step springs open to show its details while finished steps collapse to a one-line summary and the rail fills.
- [Stepper Wizard Form](https://pixelpapa.com/components/steppers/stepper-wizard-form.md) (Pro): A multi-step form that slides between steps in the direction of travel and shakes, with inline errors, when a step fails validation.
- [Timeline Horizontal Milestones](https://pixelpapa.com/components/steppers/timeline-horizontal-milestones.md) (Pro): A draggable horizontal roadmap: the track glides to centre the chosen milestone, a ring marker slides between dots, and reached milestones fill the line.
- [Timeline Vertical Activity](https://pixelpapa.com/components/steppers/timeline-vertical-activity.md): An activity feed on a vertical rail: icon nodes pop in one after another while the connecting line draws downward between them.
