Status Card
Original · freeAn order-status tracking card with a step timeline.
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/card-status.json"use client";
import { useId, useState } from "react";
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
type StepState = "done" | "current" | "upcoming";
type Step = {
key: string;
label: string;
hint: string;
at?: string;
};
type LineItem = {
name: string;
variant: string;
qty: number;
price: string;
img: string;
};
type Order = {
id: string;
tracking: string;
carrier: string;
eta: string;
total: string;
items: LineItem[];
steps: Step[];
};
const CHECK = (
<svg viewBox="0 0 24 24" fill="none" className="h-3.5 w-3.5" aria-hidden="true">
<path
d="M20 6 9 17l-5-5"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
const TRUCK = (
<svg viewBox="0 0 24 24" fill="none" className="h-4 w-4" aria-hidden="true">
<path
d="M3 6.5A1.5 1.5 0 0 1 4.5 5H14a1 1 0 0 1 1 1v9H4.5A1.5 1.5 0 0 1 3 13.5v-7ZM15 9h3.2a1 1 0 0 1 .84.46L21 12.6V15h-6V9Z"
stroke="currentColor"
strokeWidth="1.6"
strokeLinejoin="round"
/>
<circle cx="7.5" cy="17.5" r="1.9" stroke="currentColor" strokeWidth="1.6" />
<circle cx="17.5" cy="17.5" r="1.9" stroke="currentColor" strokeWidth="1.6" />
</svg>
);
function CopyIcon({ done }: { done: boolean }) {
return done ? (
<svg viewBox="0 0 24 24" fill="none" className="h-4 w-4" aria-hidden="true">
<path
d="M20 6 9 17l-5-5"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
) : (
<svg viewBox="0 0 24 24" fill="none" className="h-4 w-4" aria-hidden="true">
<rect x="9" y="9" width="11" height="11" rx="2.2" stroke="currentColor" strokeWidth="1.7" />
<path
d="M15 5.5A1.5 1.5 0 0 0 13.5 4h-8A1.5 1.5 0 0 0 4 5.5v8A1.5 1.5 0 0 0 5.5 15"
stroke="currentColor"
strokeWidth="1.7"
strokeLinecap="round"
/>
</svg>
);
}
function Chevron() {
return (
<svg viewBox="0 0 24 24" fill="none" className="h-4 w-4" aria-hidden="true">
<path
d="m6 9 6 6 6-6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function Bell() {
return (
<svg viewBox="0 0 24 24" fill="none" className="h-4 w-4" aria-hidden="true">
<path
d="M6 9a6 6 0 0 1 12 0c0 4 1.2 5.4 2 6.3.4.5 0 1.2-.7 1.2H4.7c-.7 0-1.1-.7-.7-1.2C4.8 14.4 6 13 6 9Z"
stroke="currentColor"
strokeWidth="1.7"
strokeLinejoin="round"
/>
<path d="M10 20a2 2 0 0 0 4 0" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" />
</svg>
);
}
function stateOf(index: number, current: number): StepState {
if (index < current) return "done";
if (index === current) return "current";
return "upcoming";
}
function StatusCard({ order, delivered = false }: { order: Order; delivered?: boolean }) {
const total = order.steps.length;
const [current, setCurrent] = useState(delivered ? total - 1 : 2);
const [open, setOpen] = useState(false);
const [copied, setCopied] = useState(false);
const [notify, setNotify] = useState(true);
const reduce = useReducedMotion();
const uid = useId();
const detailsId = `${uid}-details`;
const isDelivered = current >= total - 1;
const active = order.steps[current];
const progressPct = total > 1 ? (current / (total - 1)) * 100 : 0;
async function copyTracking() {
try {
await navigator.clipboard.writeText(order.tracking);
setCopied(true);
window.setTimeout(() => setCopied(false), 1600);
} catch {
setCopied(false);
}
}
return (
<div className="w-full overflow-hidden rounded-3xl border border-slate-200/80 bg-white shadow-[0_1px_0_0_rgba(15,23,42,0.04),0_18px_40px_-24px_rgba(15,23,42,0.35)] dark:border-white/10 dark:bg-slate-900 dark:shadow-[0_18px_50px_-28px_rgba(0,0,0,0.9)]">
{/* Header */}
<div className="relative flex items-start gap-4 border-b border-slate-100 px-5 py-4 dark:border-white/5">
<div
className={[
"grid h-11 w-11 shrink-0 place-items-center rounded-2xl ring-1 ring-inset",
isDelivered
? "bg-emerald-50 text-emerald-600 ring-emerald-200 dark:bg-emerald-500/10 dark:text-emerald-300 dark:ring-emerald-400/20"
: "bg-indigo-50 text-indigo-600 ring-indigo-200 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-400/20",
].join(" ")}
>
{isDelivered ? CHECK : TRUCK}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span
className={[
"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide",
isDelivered
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300"
: "bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300",
].join(" ")}
>
{!isDelivered && (
<span className="relative flex h-1.5 w-1.5">
<span className="csx-ping absolute inline-flex h-full w-full rounded-full bg-indigo-500/70" />
<span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-indigo-500" />
</span>
)}
{isDelivered ? "Delivered" : "In transit"}
</span>
</div>
<h3 className="mt-1 truncate text-[15px] font-semibold text-slate-900 dark:text-white">
{active.label}
</h3>
<p className="mt-0.5 truncate text-xs text-slate-500 dark:text-slate-400">
{isDelivered ? `Arrived ${order.eta}` : `Est. delivery ${order.eta}`} · {order.carrier}
</p>
</div>
<button
type="button"
aria-pressed={notify}
aria-label={notify ? "Turn off delivery alerts" : "Turn on delivery alerts"}
onClick={() => setNotify((v) => !v)}
className={[
"shrink-0 rounded-xl p-2 transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-indigo-500 dark:focus-visible:ring-offset-slate-900",
notify
? "bg-amber-50 text-amber-600 ring-1 ring-inset ring-amber-200 dark:bg-amber-400/10 dark:text-amber-300 dark:ring-amber-400/20"
: "text-slate-400 ring-1 ring-inset ring-transparent hover:bg-slate-50 hover:text-slate-600 dark:text-slate-500 dark:hover:bg-white/5 dark:hover:text-slate-300",
].join(" ")}
>
<Bell />
</button>
</div>
{/* Tracking number row */}
<div className="flex items-center justify-between gap-3 border-b border-slate-100 px-5 py-3 dark:border-white/5">
<div className="min-w-0">
<p className="text-[10px] font-semibold uppercase tracking-wider text-slate-400 dark:text-slate-500">
Tracking number
</p>
<p className="truncate font-mono text-[13px] font-medium text-slate-700 dark:text-slate-200">
{order.tracking}
</p>
</div>
<button
type="button"
onClick={copyTracking}
aria-label="Copy tracking number"
className={[
"inline-flex shrink-0 items-center gap-1.5 rounded-xl px-2.5 py-1.5 text-xs font-semibold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-indigo-500 dark:focus-visible:ring-offset-slate-900",
copied
? "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300"
: "bg-slate-100 text-slate-600 hover:bg-slate-200 dark:bg-white/5 dark:text-slate-300 dark:hover:bg-white/10",
].join(" ")}
>
<CopyIcon done={copied} />
{copied ? "Copied" : "Copy"}
</button>
</div>
{/* Timeline */}
<div className="px-5 py-5">
<ol className="relative ml-1">
{/* track */}
<span
aria-hidden="true"
className="absolute left-[10px] top-2 bottom-2 w-0.5 rounded-full bg-slate-200 dark:bg-white/10"
/>
{/* progress fill */}
<span
aria-hidden="true"
className="csx-fill absolute left-[10px] top-2 w-0.5 rounded-full bg-gradient-to-b from-indigo-500 to-violet-500 dark:from-indigo-400 dark:to-violet-400"
style={{ height: `calc((100% - 1rem) * ${progressPct / 100})` }}
/>
{order.steps.map((step, i) => {
const s = stateOf(i, current);
return (
<li key={step.key} className="relative flex gap-4 pb-6 last:pb-0">
<span className="relative z-10 mt-0.5 grid h-[22px] w-[22px] shrink-0 place-items-center">
<span
className={[
"grid h-[22px] w-[22px] place-items-center rounded-full ring-2 transition-colors duration-300",
s === "done"
? "bg-indigo-500 text-white ring-indigo-500 dark:bg-indigo-400 dark:ring-indigo-400"
: s === "current"
? "bg-white text-indigo-600 ring-indigo-500 dark:bg-slate-900 dark:text-indigo-300 dark:ring-indigo-400"
: "bg-white text-transparent ring-slate-300 dark:bg-slate-900 dark:ring-white/15",
].join(" ")}
>
{s === "done" ? (
CHECK
) : s === "current" ? (
<span className="relative flex h-2.5 w-2.5">
<span className="csx-ping absolute inline-flex h-full w-full rounded-full bg-indigo-400/70" />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-indigo-500 dark:bg-indigo-300" />
</span>
) : (
<span className="h-1.5 w-1.5 rounded-full bg-slate-300 dark:bg-white/20" />
)}
</span>
</span>
<div className="min-w-0 flex-1 pt-0.5">
<p
className={[
"text-sm font-semibold leading-tight",
s === "upcoming"
? "text-slate-400 dark:text-slate-500"
: "text-slate-900 dark:text-white",
].join(" ")}
>
{step.label}
</p>
<p
className={[
"mt-0.5 text-xs",
s === "upcoming"
? "text-slate-400 dark:text-slate-600"
: "text-slate-500 dark:text-slate-400",
].join(" ")}
>
{step.hint}
</p>
</div>
{step.at && s !== "upcoming" && (
<span className="shrink-0 pt-0.5 text-[11px] font-medium tabular-nums text-slate-400 dark:text-slate-500">
{step.at}
</span>
)}
</li>
);
})}
</ol>
</div>
{/* Collapsible items */}
<div className="border-t border-slate-100 dark:border-white/5">
<button
type="button"
onClick={() => setOpen((v) => !v)}
aria-expanded={open}
aria-controls={detailsId}
className="flex w-full items-center justify-between gap-3 px-5 py-3.5 text-left transition hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500 dark:hover:bg-white/[0.03]"
>
<span className="flex items-center gap-2 text-sm font-medium text-slate-700 dark:text-slate-200">
<span className="flex -space-x-2">
{order.items.slice(0, 3).map((it) => (
<span
key={it.name}
aria-hidden="true"
className="grid h-7 w-7 place-items-center rounded-full bg-indigo-100 text-[11px] font-semibold text-indigo-700 ring-2 ring-white dark:bg-indigo-500/20 dark:text-indigo-200 dark:ring-slate-900"
>
{it.name.charAt(0)}
</span>
))}
</span>
{order.items.length} item{order.items.length > 1 ? "s" : ""} · {order.total}
</span>
<span
className={[
"text-slate-400 transition-transform duration-300 dark:text-slate-500",
open ? "rotate-180" : "",
].join(" ")}
>
<Chevron />
</span>
</button>
<AnimatePresence initial={false}>
{open && (
<motion.div
id={detailsId}
key="details"
initial={reduce ? false : { height: 0, opacity: 0 }}
animate={reduce ? {} : { height: "auto", opacity: 1 }}
exit={reduce ? {} : { height: 0, opacity: 0 }}
transition={{ duration: 0.28, ease: [0.22, 1, 0.36, 1] }}
className="overflow-hidden"
>
<ul className="space-y-3 px-5 pb-4">
{order.items.map((it) => (
<li key={it.name} className="flex items-center gap-3">
<span className="h-12 w-12 shrink-0 overflow-hidden rounded-xl bg-slate-100 ring-1 ring-inset ring-slate-200 dark:bg-white/5 dark:ring-white/10">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={it.img}
alt={it.name}
loading="lazy"
draggable={false}
className="h-full w-full object-cover"
/>
</span>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium text-slate-800 dark:text-slate-100">
{it.name}
</p>
<p className="truncate text-xs text-slate-500 dark:text-slate-400">
{it.variant} · Qty {it.qty}
</p>
</div>
<span className="shrink-0 text-sm font-semibold tabular-nums text-slate-700 dark:text-slate-200">
{it.price}
</span>
</li>
))}
</ul>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Footer action */}
<div className="flex items-center gap-3 border-t border-slate-100 px-5 py-4 dark:border-white/5">
<button
type="button"
disabled={isDelivered}
onClick={() => setCurrent((c) => Math.min(c + 1, total - 1))}
className="inline-flex flex-1 items-center justify-center gap-2 rounded-xl bg-slate-900 px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-slate-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-slate-900 disabled:cursor-not-allowed disabled:bg-slate-200 disabled:text-slate-400 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-100 dark:focus-visible:ring-white dark:focus-visible:ring-offset-slate-900 dark:disabled:bg-white/10 dark:disabled:text-slate-500"
>
{isDelivered ? "Order complete" : "Advance status"}
</button>
<button
type="button"
onClick={() => setCurrent(delivered ? total - 1 : 2)}
aria-label="Reset tracking demo"
className="inline-flex items-center justify-center rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm font-semibold text-slate-600 transition hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-indigo-500 dark:border-white/10 dark:text-slate-300 dark:hover:bg-white/5 dark:focus-visible:ring-offset-slate-900"
>
Reset
</button>
</div>
</div>
);
}
const ORDER_A: Order = {
id: "WX-40821",
tracking: "1Z 999 AA1 01 2345 6784",
carrier: "UPS Ground",
eta: "Thu, Jul 16",
total: "$318.00",
items: [
{
name: "Herman Miller Aeron Chair",
variant: "Size B · Graphite",
qty: 1,
price: "$249.00",
img: "/img/gallery/g07.webp",
},
{
name: "Grovemade Desk Shelf",
variant: "Walnut · Small",
qty: 1,
price: "$49.00",
img: "/img/gallery/g12.webp",
},
{
name: "Baronfig Confidant Notebook",
variant: "Ruled · Charcoal",
qty: 2,
price: "$20.00",
img: "/img/gallery/g19.webp",
},
],
steps: [
{ key: "placed", label: "Order confirmed", hint: "Payment authorized", at: "Jul 11" },
{ key: "packed", label: "Packed at warehouse", hint: "Ohio fulfilment centre", at: "Jul 12" },
{ key: "shipped", label: "In transit", hint: "Departed Columbus, OH", at: "Jul 14" },
{ key: "out", label: "Out for delivery", hint: "On the delivery vehicle" },
{ key: "delivered", label: "Delivered", hint: "Left at front door" },
],
};
const ORDER_B: Order = {
id: "WX-40655",
tracking: "TB 2288 5510 9931 US",
carrier: "FedEx Express",
eta: "Jul 09",
total: "$142.50",
items: [
{
name: "Fellow Stagg EKG Kettle",
variant: "Matte Black · 0.9L",
qty: 1,
price: "$165.00",
img: "/img/gallery/g22.webp",
},
{
name: "Timemore Chestnut C3 Grinder",
variant: "Hand grinder",
qty: 1,
price: "$79.00",
img: "/img/gallery/g05.webp",
},
],
steps: [
{ key: "placed", label: "Order confirmed", hint: "Payment captured", at: "Jul 05" },
{ key: "packed", label: "Packed at warehouse", hint: "Reno fulfilment centre", at: "Jul 06" },
{ key: "shipped", label: "In transit", hint: "Sorted at Sacramento hub", at: "Jul 07" },
{ key: "out", label: "Out for delivery", hint: "Loaded onto van", at: "Jul 09" },
{ key: "delivered", label: "Delivered", hint: "Signed for by resident", at: "Jul 09" },
],
};
export default function CardStatus() {
return (
<section className="relative w-full bg-slate-50 px-4 py-16 sm:px-6 sm:py-20 dark:bg-slate-950">
<style>{`
@keyframes csx-ping {
75%, 100% { transform: scale(2.2); opacity: 0; }
}
.csx-ping { animation: csx-ping 1.6s cubic-bezier(0,0,0.2,1) infinite; }
.csx-fill { transition: height 500ms cubic-bezier(0.22,1,0.36,1); }
@media (prefers-reduced-motion: reduce) {
.csx-ping { animation: none; }
.csx-fill { transition: none; }
}
`}</style>
<div className="mx-auto max-w-5xl">
<div className="mx-auto mb-12 max-w-xl text-center">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
Order tracking
</p>
<h2 className="mt-2 text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl dark:text-white">
Status timeline card
</h2>
<p className="mt-3 text-sm text-slate-500 dark:text-slate-400">
Advance the status, copy the tracking number, toggle alerts and expand the order
contents. Fully keyboard accessible.
</p>
</div>
<div className="grid grid-cols-1 items-start gap-6 md:grid-cols-2">
<StatusCard order={ORDER_A} />
<StatusCard order={ORDER_B} delivered />
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
3D Tilt Cards
OriginalA responsive grid of cards that tilt in 3D toward your cursor with a light glare and lifted depth layers, powered by Framer Motion springs.

Spotlight Follow Cards
OriginalFeature cards where a soft radial spotlight and a glowing border chase the cursor on hover, with a staggered scroll reveal and an animated shimmer line.

3D Flip Cards
OriginalPricing-style cards that flip in 3D on hover or tap to reveal details on the back, keyboard accessible with a staggered entrance animation.

Animated Gradient Border Cards
OriginalCards wrapped in a live conic gradient border that rotates and speeds up on hover, with a shimmer sweep and a scrolling tag marquee.

Glow Effect Card
primitivesA reusable glow effect card for React and Tailwind, with hover and motion.

Neon Gradient Card
gradient-card.tsxA reusable neon gradient card for React and Tailwind, with hover and motion.

Spotlight Magic Card
card.tsxA reusable spotlight magic card for React and Tailwind, with hover and motion.

Tilt Spotlight Card
primitivesA reusable tilt spotlight card for React and Tailwind, with hover and motion.

Profile Card
OriginalA user profile card with avatar, stats and a follow toggle.

Product Card
OriginalA product card with image, price and add-to-cart feedback.

Pricing Single Card
OriginalA single pricing plan card with feature list and CTA.

Stat Card
OriginalA metric card with value, trend arrow and a small sparkline.

