Skip to content
Component installs need the registry setup
VegaStack Design

Timeline

Rail geometry for chronological records — a continuous connector with a node per entry; rows are Item parts, timestamps are RelativeTime.

Status
stable
Since
0.4.0
Accessibility pattern
ordered list of items

Last updated

  1. Today
  2. Deal moved to Won

    Acme renewal · $12,400

  3. Priya logged a call

    Pricing review with procurement

  4. Yesterday
  5. Proposal sent

Install

Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Timeline before installation. Retain the digest printed by this command.

pnpm exec vegastack-design verify timeline

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/timeline

Run the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.

The same command installs the registry items it composes: @vegastack/marker.

Usage

import {
  Timeline,
  TimelineItem,
  TimelineSeparator,
} from "@/components/ui/timeline";

<Timeline aria-label="Activity">
  <TimelineSeparator>Today</TimelineSeparator>
  <TimelineItem node={<StatusIcon status="done" size="sm" label="" />}>
    <Item size="sm">
      <ItemContent>
        <ItemTitle>Deal moved to Won</ItemTitle>
      </ItemContent>
      <ItemContent className="text-muted-foreground">
        <RelativeTime date={when} now={now} refresh={false} />
      </ItemContent>
    </Item>
  </TimelineItem>
</Timeline>;

Timeline is rail geometry only — the continuous vertical connector with a node per entry. Everything a row needs already exists: Item parts for the event itself, RelativeTime for timestamps, Avatar for actors, and Marker's separator variant renders the group headers. There is deliberately no TimelineTitle/TimelineDescription — that would fork Item's vocabulary.

Scope

BehaviourWhere it lives
Row anatomy (title, media…)Item parts — compose them as children
Day grouping + midnight rollHost data work; TimelineSeparator renders the computed header
Live bottom-pinned feedsMessageScroller — a different job
VirtualizationNot needed: entries carry content-visibility render skipping built in

Anatomy

Timeline is a compound component. Every exported part, with the data-slot it renders (generated from the canonical source):

Timeline — data-slot="timeline"
TimelineItem — data-slot="timeline-connector" | "timeline-content" | "timeline-item" | "timeline-node" | "timeline-rail"
TimelineSeparator — data-slot="timeline-separator"

Examples

Anatomy

Timeline              <ol> — the record
├─ TimelineSeparator  group header via Marker variant="separator"
└─ TimelineItem       <li>: aria-hidden rail (node + connector) + content
   └─ Item            the event row — Item owns the anatomy

Status nodes and linked rows

The rail node is decorative (aria-hidden) — meaning must live in the row content. A whole row becomes a link through Item's own render, not through the timeline.

  1. Sending…
  2. 4 bounced

    Marked as failed, not hidden

Day grouping

TimelineSeparator renders a computed group header through Marker's separator variant. The component imposes no grouping of its own — the host decides where a day boundary falls and emits a separator there.

  1. This week
  2. main@a1f7c2 deployed

    Production · 42s · triggered by Priya Sharma

  3. main@9c0e11 failed

    Build step exceeded the 10-minute limit

  4. Last week
  5. Ada rolled back to main@77b4de

    Reason: checkout regression on Safari

API Reference

PropTypeDefaultDescription
aria-labelstring—Accessible name for the timeline list.

Data attributes and CSS variables on Timeline

AttributeValues
data-slot"timeline"

Item

PropTypeDefaultDescription
nodeReact.ReactNode—The rail node for this entry — a StatusIcon, an Avatar, or any small glyph. Defaults to a neutral dot. Purely decorative: the entry's meaning must live in its content, so the node column is aria-hidden.

Data attributes and CSS variables on TimelineItem

AttributeValues
data-slot"timeline-connector" | "timeline-content" | "timeline-item" | "timeline-node" | "timeline-rail"

TimelineSeparator adds no props of its own — it accepts everything <li> accepts and renders its children through Marker variant="separator". Place separators between items, never as the final child (the last item's connector hides via :last-child).

Accessibility

  • Compose Item rows directly — an Item outside an ItemGroup carries no ARIA role, so the <li> stays the only list item and nothing nests invalidly. A render-composed interactive row keeps its native link/button role.
  • An <ol> of <li> entries, labelled via aria-label; group separators are real list items (an <ol> may only contain list items) rendered through Marker's separator variant — wrap their children in a heading element when the label should join the page outline.
  • The rail column (node + connector) is aria-hidden decorative geometry; an entry's status must also appear as text in its content, never by node colour alone. Superseded entries need a text signal, never strike-through alone.
  • Give each entry a <time dateTime> — RelativeTime renders one, with its deliberate no-aria-live policy (a feed that re-announces every minute is noise). RelativeTime is a focusable tooltip trigger and carries a real min-h-6 box, so it holds the 24px floor at any type size — set the timestamp column's size to whatever the row needs.
ContractStates tested
Behaviourdefault
Accessibilitysemantic-html, labeled
Visualdefault

Do / Don't

Do
Compose Item parts inside TimelineItem and pass RelativeTime a fixed `now` for deterministic SSR.
Don't
Reach for MessageScroller for a static chronological record — the live-feed primitive is dead weight without prepend-on-scroll behaviour.

On this page