Timeline
<Timeline> is a vertical list of time-stamped events tied to one entity. Use for agent run history, CRM activity tabs, audit logs — anywhere chronology is the data.
When to use
- Agent run history.
- Entity activity tab (CRM person/deal).
- Audit log scoped to one record.
Don’t use Timeline for: lists ordered by recency where time isn’t the point (use DataTable), chat-style two-pane conversations, or multi-day calendar visualization.
Default
Full body content per item, with leading dot (icon + tone), title, relative timestamp (with absolute on hover), and optional meta.
- Succeeded: Run completed alice15 calls processed in 12.4s. No retries needed.
- In progress: Run in progressStep 3 of 5 — fetching context.
- Pending: Rate-limit warningProvider returned 429 once; retried successfully.
- Failed: Run failedTool
search_kbreturned a 500. - Agent createdby bob
<Timeline aria-label="Run history"> <TimelineItem tone="success" icon={<IconCheck className="size-3.5" />} title="Run completed" timestamp={iso} meta={<UserBadge />} > 15 calls processed in 12.4s. </TimelineItem></Timeline>Compact
Single-line items, no body. Use for long audit logs.
- alice updated the agent prompt
- bob added a new tool: search_kb
- Succeeded: alice published v3
- Failed: ci failed on test 'route_a'
- agent created
<Timeline aria-label="Audit log" variant="compact"> <TimelineItem title="alice updated the agent prompt" timestamp={iso} /> …</Timeline>Grouped
Items wrapped in <TimelineGroup>s with day-style labels. The grouping is the consumer’s responsibility — Timeline doesn’t bucket dates.
- Today
- Succeeded: Deal closed: Acme — $24k
- bob commented on Acme deal"Sending the contract today."
- Yesterday
- alice updated stage → Negotiation
- Pending: Reminder fired: follow up with Acme
<Timeline aria-label="Activity feed" variant="grouped"> <TimelineGroup label="Today"> <TimelineItem … /> <TimelineItem … /> </TimelineGroup> <TimelineGroup label="Yesterday"> <TimelineItem … /> </TimelineGroup></Timeline>Pagination — onLoadMore
Pass hasMore + onLoadMore to enable cursor-paginated load-older. Timeline mounts an IntersectionObserver sentinel below the last item; when it becomes visible (the user scrolls down past the rendered events), onLoadMore fires. Re-entrant calls are suppressed while one is pending.
<Timeline aria-label="Case activity" hasMore={cursor !== null} onLoadMore={async () => { const { events: older, nextCursor } = await api.fetchEvents({ before: cursor }); setEvents((prev) => [...prev, ...older]); setCursor(nextCursor); }}> {events.map((e) => <TimelineItem key={e.id} {...e} />)}</Timeline>The loadingMoreLabel prop overrides the inline “Loading…” text. The sentinel is aria-live="polite" — screen readers announce the load message politely.
API
Timeline
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label * | string | — | Describes what events the timeline contains. Required for screen readers. |
variant | "default" | "compact" | "grouped" | "default" | Density. compact hides body content. grouped expects TimelineGroup children. |
hasMore | boolean | — | Whether more events exist past the last rendered item. Drives the IntersectionObserver sentinel. |
onLoadMore | () => Promise<void> | void | — | Async loader for older events. Re-entrant calls are suppressed while one is pending. |
loadingMoreLabel | ReactNode | "Loading…" | Inline label shown while onLoadMore is pending. |
className | string | — | Forwarded. |
TimelineItem
| Prop | Type | Default | Description |
|---|---|---|---|
title * | ReactNode | — | One-line headline. |
tone | "neutral" | "success" | "warning" | "destructive" | "current" | "neutral" | Drives the dot color and the screen-reader tone label. |
icon | ReactNode | — | Inside the dot. Pass an icon component instance. |
timestamp | string | Date | — | ISO string or Date. Renders relative + absolute via tooltip. |
meta | ReactNode | — | Right-of-title metadata (actor avatar, links). |
children | ReactNode | — | Body content. Hidden in compact variant. |
TimelineGroup
| Prop | Type | Default | Description |
|---|---|---|---|
label * | ReactNode | — | Day or section label. |
children * | ReactNode | — | TimelineItem(s). |
Design guidelines
✓ Do
- Use Timeline for "what happened to one thing." If it's about many things, you want DataTable.
- Lazy-paginate when the list grows past 50 items.
- In grouped variant, group by day. Hour-grouping creates noise; week-grouping loses the recency signal.
✗ Don't
- Wrap a Timeline in a Card if the parent surface is already a card. Flat, per Attio.
- Tint the entire row by tone. Only the dot is colored; the row text is foreground.
- Use Timeline alongside a DataTable for the same data on the same page. Pick one.
Accessibility
- Root is
<ol role="list">; items are<li role="listitem">. - Timestamps render as
<time datetime>with relative text + absolute via Tooltip. - Tones above neutral inject a visually-hidden screen-reader prefix (“Failed:”, “Succeeded:”, “In progress:”) so color is never the only signal.
- For real-time appends, set
aria-live="polite"on the parent region.
Related
DataTable— When the data is many things, sortable.InboxList— When the data is tasks.EmptyState— Use as the empty branch (composition).