Web InnoventixFreeCode

Status Card

Original · free

An order-status tracking card with a step timeline.

byWeb InnoventixReact + Tailwind
cardstatuscards
Open

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
card-status.tsx
"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}`} &middot; {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" : ""} &middot; {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} &middot; 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

motion

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 quote

Similar components

Browse all →