Patterns
PatternsTier 5 · Blocks
Order status & timeline
Status as badge or block, and the vertical progress timeline.
bunx --bun shadcn@latest add @woohl/order-statusPreview
Reference
Installs this item, its registry dependencies and the token layer.
bunx --bun shadcn@latest add @woohl/order-statusregistry/tier-5-blocks/order-status.tsx
import type { ReactElement } from "react";
import { cn } from "@/registry/tier-2-primitives/lib/utils";
/**
* OrderStatus.
*
* A status icon, a label, helper text, and a semantic colour.
*
* COLOUR IS NEVER THE ONLY SIGNAL — AND HERE THAT IS LOAD-BEARING
* "Delivered" and "Cancelled" differ by hue alone if you let them. Every
* status below carries an icon AND a word, so the state survives greyscale,
* colour-vision deficiency, and a screenshot pasted into a support thread.
*
* The statuses are the ones Woohl's order flow actually has. A status not in
* this list is a product decision, not a styling one — add it here so every
* surface renders it identically.
*/
export type OrderStatusKind =
| "placed"
| "confirmed"
| "making"
| "dispatched"
| "in-transit"
| "delivered"
| "cancelled"
| "refunded"
| "failed"
| "delayed"
| "returned";
type StatusSpec = {
label: string;
tone: "neutral" | "info" | "success" | "warning" | "danger" | "accent";
/** The default helper line. Override with `detail` when you know more. */
hint: string;
};
const STATUS: Record<OrderStatusKind, StatusSpec> = {
placed: {
label: "Order placed",
tone: "neutral",
hint: "We've got it. The maker is being notified.",
},
confirmed: {
label: "Confirmed",
tone: "info",
hint: "The maker has accepted the order.",
},
/* Made-to-order is normal on Woohl, not an exception — so it gets its own
status rather than sitting silently inside "confirmed". */
making: {
label: "Being made",
tone: "accent",
hint: "Made to order. We'll tell you when it ships.",
},
dispatched: {
label: "Dispatched",
tone: "info",
hint: "Handed to the courier.",
},
"in-transit": {
label: "On its way",
tone: "info",
hint: "Moving towards you.",
},
delivered: {
label: "Delivered",
tone: "success",
hint: "Left at the delivery address.",
},
cancelled: {
label: "Cancelled",
tone: "neutral",
hint: "This order was cancelled.",
},
refunded: {
label: "Refunded",
tone: "success",
hint: "Money is on its way back to you.",
},
failed: {
label: "Payment failed",
tone: "danger",
hint: "The payment didn't go through.",
},
delayed: {
label: "Running late",
tone: "warning",
hint: "This is later than we said. We're on it.",
},
returned: {
label: "Returned",
tone: "neutral",
hint: "Back with the maker.",
},
};
const TONE_CLASS = {
neutral: "bg-surface-sunken text-text-secondary",
accent: "bg-surface-accent-subtle text-text-accent-strong",
info: "bg-info-subtle text-info",
success: "bg-success-subtle text-success",
warning: "bg-warning-subtle text-warning",
danger: "bg-danger-subtle text-danger",
} as const;
export type OrderStatusProps = {
status: OrderStatusKind;
/** Replaces the default hint with something specific: a date, a courier. */
detail?: string;
/** `badge` for a table cell or a list row; `block` for an order page. */
variant?: "badge" | "block";
/**
* The status glyph, from the app's own icon set. Omitted renders none — the
* label always carries the meaning, so the icon is reinforcement rather than
* the only signal (see the note above about greyscale).
*/
icon?: ReactElement;
className?: string;
};
export function OrderStatus({
status,
detail,
variant = "badge",
icon,
className,
}: OrderStatusProps) {
const spec = STATUS[status];
if (variant === "badge") {
return (
<span
data-slot="order-status"
className={cn(
"rounded-pill inline-flex items-center gap-1.5 px-2.5 py-[5px]",
"font-ui text-caption font-bold whitespace-nowrap",
TONE_CLASS[spec.tone],
className,
)}
>
{icon}
{spec.label}
</span>
);
}
return (
<div
data-slot="order-status"
className={cn("font-ui flex items-start gap-3", className)}
>
<span
className={cn(
"grid size-10 flex-none place-items-center rounded-full",
TONE_CLASS[spec.tone],
)}
>
{icon}
</span>
<div>
<p className="text-body text-text-primary font-bold">{spec.label}</p>
<p className="text-label text-text-secondary mt-0.5 leading-[1.5]">
{detail ?? spec.hint}
</p>
</div>
</div>
);
}
/**
* The order timeline.
*
* A tracking page is a timeline, not a wall of order details — a buyer opening
* it is asking one question ("where is it") and the answer should be the first
* thing on the page.
*
* `<ol>` because the sequence is the meaning. Completed steps get a tick;
* the current step is marked `aria-current="step"` so it is findable without
* looking for the coloured dot.
*/
export type TimelineStep = {
status: OrderStatusKind;
/** "3 August, 4:20pm", or "Expected 6 August". */
timestamp?: string;
detail?: string;
state: "done" | "current" | "upcoming";
/** This step's glyph, from the app's own icon set. */
icon?: ReactElement;
};
export function OrderTimeline({
steps,
className,
}: {
steps: TimelineStep[];
className?: string;
}) {
return (
<ol
data-slot="order-timeline"
className={cn("font-ui flex flex-col", className)}
>
{steps.map((step, index) => {
const spec = STATUS[step.status];
const last = index === steps.length - 1;
return (
<li
key={`${step.status}-${index}`}
aria-current={step.state === "current" ? "step" : undefined}
className="flex gap-3.5"
>
<div className="flex flex-col items-center">
<span
className={cn(
"grid size-7 flex-none place-items-center rounded-full",
step.state === "upcoming"
? "border-border-strong bg-surface-raised text-text-muted border"
: step.state === "current"
? "bg-surface-accent text-text-on-accent"
: "bg-success-subtle text-success",
)}
>
{step.icon}
</span>
{!last && (
<span
aria-hidden
className={cn(
"w-px flex-1",
step.state === "done"
? "bg-success-border"
: "bg-border-subtle",
)}
/>
)}
</div>
<div className={cn("pb-6", last && "pb-0")}>
<p
className={cn(
"text-body font-bold",
step.state === "upcoming"
? "text-text-muted"
: "text-text-primary",
)}
>
{spec.label}
</p>
{step.timestamp && (
<p className="text-caption text-text-muted mt-0.5">
{step.timestamp}
</p>
)}
{step.detail && (
<p className="text-label text-text-secondary mt-1 leading-[1.5]">
{step.detail}
</p>
)}
</div>
</li>
);
})}
</ol>
);
}
OrderStatusProps
| Prop | Type | Default |
|---|---|---|
status* | OrderStatusKind | — |
className | string | — |
detailReplaces the default hint with something specific: a date, a courier. | string | — |
iconThe status glyph, from the app's own icon set. Omitted renders none — the label always carries the meaning, so the icon is reinforcement rather than the only signal (see the note above about greyscale). | ReactElement | — |
variant`badge` for a table cell or a list row; `block` for an order page. | "badge" | "block" | — |
Registry items
npm packages
None beyond React itself.