A coil of pills that keeps one item in focus and lets the rest twist away, turned either by a timer or by the page scroll.
Status Timeline
A colour-coded step tracker for orders, deployments and onboarding, still at rest and animated only when a step advances.
StatusTimeline renders a fixed sequence of steps and marks where the process currently stands. Pass an activeStep index and it derives the rest — earlier steps turn green, the one in flight turns blue, the rest stay grey behind a dashed ring — or pin a state per step to describe a run that is waiting on someone or has failed outright. At rest it is a still drawing: nothing loops and nothing breathes. The motion is spent on the one moment worth showing — move activeStep on and the connector above the cleared step draws downward, the marker it reaches cross-fades into its new ring, and a single pulse leaves the step now in flight. It lays out vertically as a tracking card or horizontally as a wizard header, and every hue is backed by a glyph and a text label, so the state never rests on colour alone.
Preview
One index decides everything. Advance it and the line draws down, the next marker cross-fades into blue and pulses once — then everything goes still again.
- 17 Nov, 13:45
Order confirmed, Completed
Payment captured, order placed.
- 17 Nov, 16:02
Packed, Completed
Leaving the Rotterdam warehouse.
- 18 Nov, 08:20
In transit, In progress
Handed to the courier.
- 19 Nov, 11:07
Delivered, Not started
Signed for at the front desk.
Past the last index every step is complete, and the header chip goes green with it.
- 17 Nov, 13:45
Order confirmed, Completed
Payment captured, order placed.
- 17 Nov, 16:02
Packed, Completed
Leaving the Rotterdam warehouse.
- 18 Nov, 08:20
In transit, Completed
Handed to the courier.
- 19 Nov, 11:07
Delivered, Completed
Signed for at the front desk.
orientation="horizontal"The same steps laid along a rule — a checkout or wizard header.
Cart, Completed
Address, In progress
Payment, Not started
Review, Not started
state: "waiting"Amber for a step that is neither moving nor broken — an approval, a review, a queue.
Account created, Completed
Workspace configured, Completed
Identity review, Waiting
Usually clears within an hour.
Invite your team, Not started
state: "blocked", variant="plain"Red for a step that failed, with the card and header dropped so the list can sit in a surface you own.
- 12s
Install, Completed
- 31s
Typecheck, Completed
- 1m 04s
Test, Blocked
4 of 212 assertions failed.
Deploy, Not started
Installation
Install Status Timeline with the shadcn CLI. The registry item resolves its own dependencies and writes the file to components/joinui/status-timeline.tsx.
pnpm dlx shadcn@latest add @joinui/status-timelineUsage
import { StatusTimeline } from "@/components/joinui/status-timeline"
export function Example() {
return (
<StatusTimeline
label="Delivery"
activeStep={2}
steps={[
{ id: "confirmed", title: "Order confirmed", timestamp: "17 Nov, 13:45" },
{ id: "packed", title: "Packed", timestamp: "17 Nov, 16:02" },
{ id: "transit", title: "In transit", timestamp: "18 Nov, 08:20" },
{ id: "delivered", title: "Delivered" },
]}
/>
)
}Props
StatusTimeline
| Prop | Type | Default | Description |
|---|---|---|---|
| steps (required) | StatusTimelineStep[] | — | The sequence to render, in order. |
| activeStep | number | 0 | Index of the step in flight. Earlier steps resolve to complete, later ones to pending. Pass steps.length to complete the whole sequence. |
| orientation | "vertical" | "horizontal" | "vertical" | Vertical stacks the steps as a tracking card; horizontal lays them along a rule as a wizard header. |
| size | "sm" | "md" | "md" | Marker, type and spacing scale. |
| label | string | "Timeline" | Header eyebrow, and the accessible name of the list when the header is hidden. |
| status | string | — | Overrides the header chip copy, which is otherwise rolled up from the resolved steps. |
| variant | "card" | "plain" | "card" | Plain drops the surrounding rule, background and padding so the list can sit inside your own container. |
| showHeader | boolean | true | Renders the eyebrow and status chip above the steps. |
| footer | React.ReactNode | — | Content placed below a rule — an action, a note, a summary row. |
| className | string | — | Merged onto the root element through `cn`. |
StatusTimelineStep
Shape of a single entry in the `steps` array.
| Prop | Type | Default | Description |
|---|---|---|---|
| id (required) | string | — | Stable identity for the rendered list item. |
| title (required) | string | — | Step heading. |
| description | string | — | Supporting line below the title. |
| timestamp | string | — | Trailing meta, set in mono with tabular figures — a time, a duration, an ETA. |
| state | "complete" | "current" | "waiting" | "pending" | "blocked" | — | Pins the state instead of deriving it from activeStep, and picks the hue: green for complete, blue for current, amber for waiting, grey for pending, red for blocked. |
| icon | React.ReactNode | — | Replaces the default state glyph inside the marker. Sized by the component, so pass a bare icon element. |
Dependencies
npm packages
- motion
- lucide-react
Registry items
- utils
Installed automatically by the CLI when they are missing.
Accessibility
- Steps render as an ordered list, so assistive technology reports both position and total.
- The step in flight carries `aria-current="step"`.
- Colour is never the only signal, which is what satisfies WCAG 1.4.1: every state also has its own glyph — a tick, a pip, a clock, a cross — and appends a visually hidden label reading “Completed”, “In progress”, “Waiting”, “Not started” or “Blocked”.
- A blocked step additionally shifts its title to the critical hue, so the failure is findable without reading every marker.
- The list is named by the header eyebrow through `aria-labelledby`, or by `label` when the header is hidden.
- Markers, trails and the arrival ring are `aria-hidden` and non-interactive; only content you pass to `footer` enters the tab order.
- Nothing animates on mount or loops at rest, so the component never competes with the page for attention; the advance transition animates `transform` and `opacity` only, and resolves instantly under `prefers-reduced-motion`.
- Both themes are covered by the tokens rather than by `dark:` variants, so the component keeps its contrast inside a forced-theme subtree.
Keyboard interactions
Status Timeline renders no interactive controls of its own, so it adds nothing to the tab order.
Customization
A horizontal wizard header
Drop the descriptions and switch orientation to get a checkout stepper that spans its container.
<StatusTimeline
label="Checkout"
orientation="horizontal"
size="sm"
activeStep={1}
steps={[
{ id: "cart", title: "Cart" },
{ id: "address", title: "Address" },
{ id: "payment", title: "Payment" },
{ id: "review", title: "Review" },
]}
/>Pin a step that failed, or one that is waiting
A state on the step wins over the one derived from activeStep, so a run that stalled can be described exactly. Blocked turns the marker and the title red; waiting turns the marker amber for something that is neither moving nor broken, like an approval sitting in a queue. Either way the header chip picks the state up on its own.
<StatusTimeline
label="Pipeline"
steps={[
{ id: "install", title: "Install", timestamp: "12s", state: "complete" },
{ id: "typecheck", title: "Typecheck", timestamp: "31s", state: "complete" },
{
id: "test",
title: "Test",
description: "4 of 212 assertions failed.",
state: "blocked",
},
{ id: "deploy", title: "Deploy", state: "pending" },
]}
/>
<StatusTimeline
label="Onboarding"
steps={[
{ id: "account", title: "Account created", state: "complete" },
{
id: "review",
title: "Identity review",
description: "Usually clears within an hour.",
state: "waiting",
},
{ id: "invite", title: "Invite your team", state: "pending" },
]}
/>Give each step its own glyph
An icon replaces the default state glyph while the ring and the hidden state label keep carrying the meaning — a dashed grey circle before the step happens, a tinted one after. The component sizes it, so pass the element bare.
import { MapPin, Package, ShoppingBag, Truck } from "lucide-react"
<StatusTimeline
activeStep={2}
steps={[
{ id: "confirmed", title: "Order confirmed", icon: <ShoppingBag /> },
{ id: "packed", title: "Packed", icon: <Package /> },
{ id: "transit", title: "In transit", icon: <Truck /> },
{ id: "delivered", title: "Delivered", icon: <MapPin /> },
]}
/>Sit inside your own container
The plain variant removes the rule, background and padding, and the header can go with it — useful inside a drawer or an existing card.
<div className="rounded-soft-lg border border-border p-6">
<h3 className="mb-4 text-sm font-medium">Onboarding</h3>
<StatusTimeline
variant="plain"
showHeader={false}
label="Onboarding"
size="sm"
activeStep={1}
steps={steps}
/>
</div>Add an action below the steps
Anything passed to footer renders below a rule. It is the only part of the component that can take focus, so it is where a call to action belongs.
<StatusTimeline
label="Delivery"
activeStep={steps.length}
steps={steps}
footer={
<button
type="button"
className="inline-flex w-full items-center justify-center gap-1.5 rounded-full bg-positive-soft px-3 py-1.5 text-xs font-medium text-positive"
>
<Star aria-hidden="true" className="size-3.5" />
Rate this delivery
</button>
}
/>Retint the states
The states read the component palette rather than literal colours, so a brand hue is a token override — no props to thread and no variants to add. Redeclare a family in both themes and every step, chip and trail follows.
/* app/globals.css */
:root,
.light {
/* A violet in-flight state instead of the default blue. */
--info: oklch(0.5 0.19 292);
--info-soft: oklch(0.965 0.025 292);
--info-foreground: oklch(0.99 0.005 292);
}
.dark {
--info: oklch(0.76 0.15 292);
--info-soft: oklch(0.255 0.06 292);
--info-foreground: oklch(0.16 0.04 292);
}