Skip to content

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.

import from "@na/ui/components/Timeline" ▶ Open in Storybook packages/ui/src/components/Timeline.tsx

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.

  1. Succeeded: Run completed alice
    15 calls processed in 12.4s. No retries needed.
  2. In progress: Run in progress
    Step 3 of 5 — fetching context.
  3. Pending: Rate-limit warning
    Provider returned 429 once; retried successfully.
  4. Failed: Run failed
    Tool search_kb returned a 500.
  5. 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.

  1. alice updated the agent prompt
  2. bob added a new tool: search_kb
  3. Succeeded: alice published v3
  4. Failed: ci failed on test 'route_a'
  5. 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.

  1. Today
    1. Succeeded: Deal closed: Acme — $24k
    2. bob commented on Acme deal
      "Sending the contract today."
  2. Yesterday
    1. alice updated stage → Negotiation
    2. 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.
  • DataTable — When the data is many things, sortable.
  • InboxList — When the data is tasks.
  • EmptyState — Use as the empty branch (composition).

▶ Open Timeline stories in Storybook