All blocks
Async & Statusbeginner
Order Tracking
A shipment tracker driven by typed per-step metadata (carrier, location, ETA).
Installation
Add it with the shadcn CLI, open it in v0, or read the source.
$ npx shadcn@latest add https://stepperize.com/r/order-tracking.jsonDependencies
- @stepperize/react
- lucide-react
Requirements
- React 18 or later
- Tailwind CSS
Source
import { defineStepper } from "@stepperize/react";
import { Check, Package, Truck, Warehouse } from "lucide-react";
import type { ComponentType } from "react";
import { useState } from "react";
// `location`, `detail`, and `eta` are typed metadata on each step. They're read
// back off `stepper.current` with full inference — no separate status table.
const steps = [
{
id: "placed",
title: "Order placed",
icon: Check,
location: "Online",
detail: "We received your order.",
eta: "Ships in 1 day",
},
{
id: "packed",
title: "Packed",
icon: Package,
location: "Newark, NJ",
detail: "Your parcel is packed and labelled.",
eta: "Picked up today",
},
{
id: "transit",
title: "In transit",
icon: Truck,
location: "Columbus, OH",
detail: "Out with the carrier, moving your way.",
eta: "Arrives tomorrow",
},
{
id: "delivered",
title: "Delivered",
icon: Warehouse,
location: "Your address",
detail: "Left at the front door.",
eta: "Delivered",
},
] as const;
const icons: Record<
string,
ComponentType<{ className?: string }>
> = Object.fromEntries(steps.map((s) => [s.id, s.icon]));
const { Stepper } = defineStepper(steps);
export function OrderTrackingBlock() {
const [confirmed, setConfirmed] = useState(false);
return (
<Stepper.Root
defaultStep="transit"
className="w-full max-w-lg rounded-xl border bg-background p-6 shadow-sm"
>
{({ stepper }) => (
<>
<div className="mb-6 flex items-center justify-between">
<div>
<p className="text-sm font-semibold">Order #4815</p>
<p className="text-xs text-muted-foreground">
{stepper.current.eta}
</p>
</div>
<span className="rounded-full bg-primary/10 px-2.5 py-1 text-xs font-medium text-primary">
{stepper.current.title}
</span>
</div>
<Stepper.List className="flex items-start">
<Stepper.Items>
{(step, index) => {
const Icon = icons[step.id];
return (
<div
key={step.id}
className="flex flex-1 items-start last:flex-none"
>
<Stepper.Item
step={step.id}
className="flex flex-col items-center gap-2"
>
<Stepper.Indicator className="group grid size-10 place-items-center rounded-full border-2 transition-colors data-[status=active]:border-primary data-[status=active]:bg-primary data-[status=active]:text-primary-foreground data-[status=previous]:border-primary data-[status=previous]:bg-primary data-[status=previous]:text-primary-foreground data-[status=upcoming]:border-border data-[status=upcoming]:bg-background data-[status=upcoming]:text-muted-foreground">
<Check className="hidden size-5 group-data-[status=previous]:block" />
<Icon className="size-5 group-data-[status=previous]:hidden" />
</Stepper.Indicator>
<Stepper.Title className="w-20 text-center text-xs font-medium" />
</Stepper.Item>
{index < stepper.count - 1 && (
<div
className="mx-1 mt-5 h-0.5 flex-1 rounded bg-border data-[done=true]:bg-primary"
data-done={index < stepper.index}
/>
)}
</div>
);
}}
</Stepper.Items>
</Stepper.List>
{/* Detail panel driven by the current step's typed metadata. */}
<div className="mt-6 flex items-start justify-between gap-3 rounded-lg border bg-muted/30 p-3 text-sm">
<div>
<p className="font-medium">
{confirmed
? "Delivery confirmed. Thanks for checking in."
: stepper.current.detail}
</p>
<p className="mt-0.5 text-xs text-muted-foreground">
{stepper.current.location}
</p>
</div>
<span className="shrink-0 text-xs font-medium text-primary">
{stepper.current.eta}
</span>
</div>
<Stepper.Actions className="mt-6 flex justify-center gap-2">
<button
type="button"
onClick={() => stepper.prev()}
disabled={!stepper.canPrev}
className="inline-flex h-8 items-center rounded-lg border bg-background px-3 text-sm font-medium transition-colors hover:bg-muted disabled:pointer-events-none disabled:opacity-50"
>
Rewind
</button>
{confirmed ? (
<button
type="button"
onClick={() => {
setConfirmed(false);
stepper.reset();
}}
className="inline-flex h-8 items-center rounded-lg bg-primary px-3 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Track again
</button>
) : stepper.isLast ? (
<button
type="button"
onClick={() => setConfirmed(true)}
className="inline-flex h-8 items-center rounded-lg bg-primary px-3 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Confirm delivery
</button>
) : (
<button
type="button"
onClick={() => stepper.next()}
className="inline-flex h-8 items-center rounded-lg bg-primary px-3 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Advance status
</button>
)}
</Stepper.Actions>
</>
)}
</Stepper.Root>
);
}
When to use it
Read-only status timelines — order/shipment tracking where each stage carries typed metadata.
Accessibility
Each stage states its status in text; completed vs. upcoming is conveyed beyond color.
Customization
Per-stage metadata (carrier, ETA) is typed off the step definition; bind the active stage to live status.
Related blocks
CI/CD PipelineBuild, test, deploy, verify pipeline status with typed stage metadata.Approval TimelineA vertical audit timeline where each step carries typed metadata (actor, role, SLA hours) read straight off `step` and `stepper.current` with full inference.Course PlayerA lesson playlist with typed per-lesson metadata and a final quiz.