Display
Timeline
A contract-driven vertical timeline with done, active and upcoming milestones and four data states.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/timeline.jsonPrompt
Build a React + TypeScript + Tailwind "Timeline" component (lucide-react
Check) with zod.
Contract
- A zod schema is the single source of truth:
{ status: "loading" | "empty" | "error" | "ready";
items: { id, title, description?, timestamp (ISO 8601 string),
status: "done" | "active" | "upcoming" }[] }.
- Component props = z.infer of the schema plus onRetry?: () => void and
className.
Behavior
- Four first-class branches: loading (three skeleton rows mirroring the real
row anatomy — a dot plus a title bar and a description bar, joined by the
connector), empty ("Nothing here yet" plus one supporting line), error
(message plus a "Try again" button rendered only when onRetry exists),
ready (the timeline itself).
- Ready: items render in given order as one vertical thread. Each row is a
node dot on the left and content on the right; a 1px connector fills from
each dot down to the next row's dot. The last row draws no trailing line,
so the thread starts and ends exactly at the data.
- Node semantics by item status: done = solid primary dot containing a small
check (size-3); active = hollow primary-border dot with a ping halo —
decorative, aria-hidden, hidden under prefers-reduced-motion; upcoming =
hollow dot with a muted 40%-opacity border.
- Timestamps are ISO strings formatted with Intl.DateTimeFormat to
"Jul 21, 2026", rendered in a <time dateTime={iso}> element.
- Retry stays the consumer's: the error button only calls onRetry — no fetch
logic lives in the component.
Rendering & styling
- Semantic tokens only: bg-primary / text-primary-foreground for done nodes,
border-primary for active, border-muted-foreground/40 for upcoming,
bg-border for the connector, bg-muted for skeleton bars,
text-muted-foreground for timestamps and descriptions. cn() merges the
consumer className into the root.
- Title is text-sm font-medium with the text-xs timestamp on the same
baseline row (flex-wrap so narrow columns wrap instead of overflow); the
optional description sits below in text-sm text-muted-foreground.
- A sr-only status word inside each row keeps done/active/upcoming readable
to screen readers while the dots stay decorative.
Customization levers
- Density: row spacing is a single pb-* on non-last rows and the dot a single
size-* pair — tighten both for compact audit logs, widen for marketing.
- Alignment: content sits right of the rail by default; for right-aligned or
alternating (zigzag) layouts, keep the node column and mirror the content
column per row — the contract does not change.
- Icons: the done check is one slot — extend the contract with an optional
per-item icon field and render it inside the node for richer event types.
- Status colors: done/active ride the primary token; remap to chart tokens
(var(--chart-N)) when the host reserves primary for CTAs.
- Timestamp format: swap the Intl.DateTimeFormat options (or a relative-time
formatter) without touching layout — the contract keeps raw ISO strings.Concepts
- Status-encoded nodes — each dot's fill, border and motion encode done vs active vs upcoming, so the eye reads progress along the thread without reading a word of copy.
- Continuous thread, exact ends — every row owns the connector segment below its own dot and the last row omits it; the rail starts and ends at the data instead of floating past it.
- Contract-driven four states — timelines are fed by APIs (releases, orders, audits) that can be slow, empty or down;
statusmakes those paths first-class branches, not afterthought&&s. - Reduced-motion-safe emphasis — the active ping is a purely decorative halo layer: aria-hidden and removed under prefers-reduced-motion, while the hollow primary dot still marks the current step.
- Skeleton mirrors anatomy — loading repeats the dot-plus-two-bars silhouette three times on the same rail, so the layout doesn't jump when real rows arrive.