# Stepper Order Tracking — React Stepper Component

> 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.

- Category: [Steppers & Timelines](https://pixelpapa.com/components/steppers.md)
- Tags: stepper, order, tracking, shipping, svg path
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/steppers/stepper-order-tracking

## Install

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

The file is written to `components/pixelpapa/stepper-order-tracking.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 { Truck } from "lucide-react";
import { animate, AnimatePresence, motion, useMotionValue, useReducedMotion, useTransform } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";

export type TrackingStage = { label: string; detail: string; time: string };

type OrderTrackerProps = {
  stages: TrackingStage[];
  /** Index of the stage the shipment has reached. */
  current: number;
  orderId: string;
  eta: string;
  className?: string;
};

const W = 560;
const H = 132;
const ROUTE = "M 34 84 C 96 84, 104 30, 166 34 S 248 104, 300 88 S 388 22, 440 40 S 500 84, 526 62";

export function OrderTracker({ stages, current, orderId, eta, className }: OrderTrackerProps) {
  const reduce = useReducedMotion();
  const path = useRef<SVGPathElement>(null);
  const progress = useMotionValue(0);
  const [stops, setStops] = useState<{ x: number; y: number }[]>([]);
  const last = stages.length - 1;

  function at(p: number) {
    const el = path.current;
    if (!el) return { x: 34, y: 84 };
    return el.getPointAtLength(Math.max(0, Math.min(1, p)) * el.getTotalLength());
  }

  // The marker is plain HTML positioned in % so it stays crisp at any size.
  const left = useTransform(progress, (p) => `${(at(p).x / W) * 100}%`);
  const top = useTransform(progress, (p) => `${(at(p).y / H) * 100}%`);
  const tilt = useTransform(progress, (p) => {
    const a = at(p - 0.01);
    const b = at(p + 0.01);
    return (Math.atan2(b.y - a.y, b.x - a.x) * 180) / Math.PI;
  });

  useEffect(() => {
    setStops(stages.map((_, i) => at(i / last)));
  }, [stages, last]);

  useEffect(() => {
    const controls = animate(progress, current / last, { duration: reduce ? 0 : 1.1, ease: [0.65, 0, 0.35, 1] });
    return () => controls.stop();
  }, [current, last, progress, reduce]);

  const stage = stages[current];
  const delivered = current === last;

  return (
    <div className={cn("w-full rounded-2xl border border-border bg-card p-5 shadow-sm sm:p-6", className)}>
      <div className="flex items-start justify-between gap-4">
        <div>
          <p className="font-mono text-xs text-muted-foreground">Order {orderId}</p>
          <p className="mt-1 text-lg font-semibold tracking-tight text-card-foreground">{delivered ? "Delivered" : `Arriving ${eta}`}</p>
        </div>
        <span
          className={cn(
            "rounded-full px-2.5 py-1 text-xs font-medium transition-colors duration-500",
            delivered ? "bg-emerald-500/10 text-emerald-700 dark:text-emerald-400" : "bg-primary/10 text-primary",
          )}
        >
          {delivered ? "Complete" : "On schedule"}
        </span>
      </div>

      <div className="relative mt-3 mb-9" style={{ aspectRatio: `${W} / ${H}` }}>
        <svg viewBox={`0 0 ${W} ${H}`} fill="none" aria-hidden className="absolute inset-0 size-full overflow-visible">
          <path ref={path} d={ROUTE} strokeWidth={2} strokeLinecap="round" strokeDasharray="1 7" className="stroke-muted-foreground/50" />
          <motion.path d={ROUTE} strokeWidth={2.5} strokeLinecap="round" style={{ pathLength: progress }} className="stroke-primary" />
        </svg>

        <ol>
          {stops.map((pt, i) => {
            const done = i <= current;
            return (
              <li
                key={stages[i].label}
                aria-current={i === current ? "step" : undefined}
                style={{ left: `${(pt.x / W) * 100}%`, top: `${(pt.y / H) * 100}%` }}
                className="absolute flex -translate-x-1/2 -translate-y-1/2 flex-col items-center"
              >
                <motion.span
                  initial={false}
                  animate={{ scale: done ? 1 : 0.8 }}
                  transition={{ type: "spring", stiffness: 400, damping: 18, delay: done && !reduce ? 0.75 : 0 }}
                  className={cn(
                    "block size-3.5 rounded-full border-2 transition-colors delay-700 duration-300",
                    done ? "border-primary bg-primary" : "border-muted-foreground/40 bg-card",
                  )}
                />
                <span
                  className={cn(
                    "absolute top-6 text-xs whitespace-nowrap transition-colors delay-700 duration-300",
                    i === current ? "font-semibold text-foreground" : done ? "text-foreground/80" : "text-muted-foreground",
                  )}
                >
                  {stages[i].label}
                </span>
              </li>
            );
          })}
        </ol>

        <motion.div aria-hidden style={{ left, top }} className="absolute z-10 -translate-x-1/2 -translate-y-1/2">
          <motion.div
            animate={delivered ? { scale: [1, 1.18, 1] } : { scale: 1 }}
            transition={{ duration: 0.5, delay: delivered ? 1.1 : 0 }}
            className="flex size-10 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg ring-4 shadow-primary/40 ring-card"
          >
            <motion.span style={{ rotate: tilt }} className="block">
              <Truck className="size-[18px]" />
            </motion.span>
          </motion.div>
        </motion.div>
      </div>

      <div aria-live="polite" className="flex min-h-12 items-center gap-3 overflow-hidden rounded-xl bg-muted/60 px-4 py-2.5">
        <span className="relative flex size-2 shrink-0">
          {!delivered && <span className="absolute inset-0 animate-ping rounded-full bg-primary/60 motion-reduce:hidden" />}
          <span className={cn("relative size-2 rounded-full", delivered ? "bg-emerald-500" : "bg-primary")} />
        </span>
        <AnimatePresence mode="wait" initial={false}>
          <motion.p
            key={current}
            initial={{ opacity: 0, y: 8 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -8 }}
            transition={{ duration: 0.18 }}
            className="flex min-w-0 flex-1 items-baseline justify-between gap-4 text-sm"
          >
            <span className="truncate text-foreground">{stage.detail}</span>
            <time className="shrink-0 text-xs text-muted-foreground">{stage.time}</time>
          </motion.p>
        </AnimatePresence>
      </div>
    </div>
  );
}

const stages: TrackingStage[] = [
  { label: "Ordered", detail: "Order confirmed and payment received", time: "Mon 09:14" },
  { label: "Packed", detail: "Packed at the Rotterdam fulfilment centre", time: "Mon 16:40" },
  { label: "In transit", detail: "Left the Antwerp sorting hub", time: "Tue 06:42" },
  { label: "Out for delivery", detail: "With your courier, Daan — 7 stops away", time: "Thu 08:05" },
  { label: "Delivered", detail: "Handed to resident at the front door", time: "Thu 11:32" },
];

export default function StepperOrderTrackingDemo() {
  const [current, setCurrent] = useState(2);
  const done = current === stages.length - 1;

  return (
    <div className="w-full max-w-xl">
      <OrderTracker stages={stages} current={current} orderId="PP-48213" eta="Thursday, Nov 14" />
      <div className="mt-4 flex justify-center">
        <button
          type="button"
          onClick={() => setCurrent(done ? 0 : current + 1)}
          className="h-9 rounded-full border border-border bg-background px-4 text-sm font-medium text-foreground outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/60"
        >
          {done ? "Replay shipment" : "Simulate next update"}
        </button>
      </div>
    </div>
  );
}
```

## AI prompt

```text
Add the "Stepper Order Tracking" component from PixelPapa UI to this project.

What it is: 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.
Category: Steppers & Timelines. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/stepper-order-tracking.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/stepper-order-tracking.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/stepper-order-tracking" 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 Horizontal Progress](https://pixelpapa.com/components/steppers/stepper-horizontal-progress.md): A horizontal stepper where the connector line fills toward the next step and each completed circle draws its checkmark.
- [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.
