Display

Timeline

A contract-driven vertical timeline with done, active and upcoming milestones and four data states.

Preview in your theme

Loading preview…

"use client"

import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
import type { TimelineData, TimelineItem } from "./timeline.contract"

export interface TimelineProps extends TimelineData {
  onRetry?: () => void
  className?: string
}

const dateFormat = new Intl.DateTimeFormat("en-US", {
  month: "short",
  day: "numeric",

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/timeline.json

Prompt

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; status makes 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.

On This Page