Timeline
Rail geometry for chronological records — a continuous connector with a node per entry; rows are Item parts, timestamps are RelativeTime.
- Status
- Since
0.4.0- Accessibility pattern
- ordered list of items
Last updated
- Deal moved to Won
Acme renewal · $12,400
- Priya logged a call
Pricing review with procurement
- 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 timelineInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/timelineRun 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
| Behaviour | Where it lives |
|---|---|
| Row anatomy (title, media…) | Item parts — compose them as children |
| Day grouping + midnight roll | Host data work; TimelineSeparator renders the computed header |
| Live bottom-pinned feeds | MessageScroller — a different job |
| Virtualization | Not 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):
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 anatomyStatus 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.
- Sending…
- 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.
- main@a1f7c2 deployed
Production · 42s · triggered by Priya Sharma
- main@9c0e11 failed
Build step exceeded the 10-minute limit
- Ada rolled back to main@77b4de
Reason: checkout regression on Safari
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | — | Accessible name for the timeline list. |
Data attributes and CSS variables on Timeline
| Attribute | Values |
|---|---|
data-slot | "timeline" |
Item
| Prop | Type | Default | Description |
|---|---|---|---|
node | React.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
| Attribute | Values |
|---|---|
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
Itemrows directly — anItemoutside anItemGroupcarries no ARIA role, so the<li>stays the only list item and nothing nests invalidly. Arender-composed interactive row keeps its nativelink/buttonrole. - An
<ol>of<li>entries, labelled viaaria-label; group separators are real list items (an<ol>may only contain list items) rendered throughMarker'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-hiddendecorative 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>—RelativeTimerenders one, with its deliberate no-aria-livepolicy (a feed that re-announces every minute is noise).RelativeTimeis a focusable tooltip trigger and carries a realmin-h-6box, so it holds the 24px floor at any type size — set the timestamp column's size to whatever the row needs.
| Contract | States tested |
|---|---|
| Behaviour | default |
| Accessibility | semantic-html, labeled |
| Visual | default |