← All proposals

Timeline

Net-new, in two parts.

Net-new componentOpen in Storybook

proposals/Timeline/HANDOFF.md122 lines · View on GitHub

Handoff: Timeline (with TimelineItem)

What and why

A product often needs a log of events over time: deploys, checks, approvals, an agent joining and working. PDS has nothing for it, so teams draw the rail, the times and the status icons by hand. This proposal is two components: TimelineItem, one event or piece of work (an optional status icon, a label, quiet secondary text, a time, optional expandable detail and a slot for content under it), and Timeline, the list that owns list semantics, spacing and an optional rail joining the icons. TimelineItem is the former AgentStep, renamed and generalized (2026-10-16): an agent run is one region of a larger log, so the row is generic and the agent flavor is a boolean, isAgentic.

Dividers, a Chain of Thought or a Thinking State can sit between items. The full "agent joins an event log" composition is a recipe, Recipes/Event log with agent (proposals/EventLogWithAgent/), not a component.

  • Figma / Jira: Figma page below under Preview; Jira none. Sources were designer screenshots (2026-10-12 and 2026-10-16): a deploy log with a "Zappy engaged" divider, an agent sandbox investigation, and a plain neutral log.
  • Context: context.md

Exploration and decisions

  • Rename AgentStep to TimelineItem rather than keep a wrapper or build a second row. One row, with isAgentic (the same boolean convention as Button, IconButton and MenuButton). Chain of Thought now renders a Timeline for its body.
  • Timeline is a thin list. It earns its place because the rail must run through the gap between items, and something has to own the ol/li semantics. Each child becomes a list item.
  • Rail is optional (hasRail, default false). It joins only neighbouring TimelineItems, so a divider, a Chain of Thought or a Thinking State between items breaks it. It is drawn by each entry's ::after, in em, so it follows the item's text size.
  • The full event log is a recipe, not a component. Where the agent starts and stops, the divider wording and which entries are agentic are screen decisions.
  • isAgentic changes the status icon color to the discovery foreground; errors stay critical. time sits at the end of the row; an expandable item with a time spans the full row and the chevron comes after the time.
  • Names: Timeline / TimelineItem over ActivityLog and Step (Step implies a sequence).
  • Carried over from AgentStep (see decisions.md): optional status, configurable iconName, tool calls are items with detail, a children slot under the row, Tag for file identifiers, detail text inherits the item's size.

API

Timeline

Prop Type Default Purpose
children ReactNode none What goes in the list. Each child becomes a list item.
className string none Additional class names.
hasRail boolean false Draw a vertical rail joining neighbouring TimelineItems. Decorative.
size 'xs' | 's' 'xs' Text size of the items, applied as a pds-copy-* class on the list.

TimelineItem

Prop Type Default Purpose
children ReactNode none Content under the item, always visible.
className string none Additional class names.
detail ReactNode none Expandable content. An item with detail becomes a toggle.
iconName IconProps['iconName'] none Replaces the status icon. The active spinner still shows.
isAgentic boolean false The agent's own item: the status icon takes the agent color.
isQuiet boolean false Quiet text for secondary rows.
label ReactNode required What happened or is happening.
labelStrings Record<TimelineItemStatus, string> In progress, Done, Failed, Pending Translatable screen-reader text for the status.
secondaryText ReactNode none Quiet supporting text after the label.
size 'xs' | 's' none (inherits) Text size, applied as a pds-copy-* class on the item. Unset inherits, for example from a Timeline's size.
status 'active' | 'done' | 'error' | 'pending' none Where the item is at. Without it (and without iconName) there is no icon.
time string none When it happened, formatted by the consumer. Shown at the end of the row.
timeDateTime string none Machine-readable value for the time element.

Variants and states: with and without rail; the four statuses; isAgentic on and off; with and without time, secondary text, detail and content; items with a custom icon.

Tokens used

  • --pds-animation-transition-rotation
  • --pds-border-radius-default
  • --pds-border-width-default
  • --pds-border-width-outline
  • --pds-color-border-separator
  • --pds-color-foreground-default
  • --pds-color-foreground-default-secondary
  • --pds-color-interactive-focus
  • --pds-color-status-critical-foreground
  • --pds-color-status-discovery-foreground
  • --pds-color-status-success-foreground
  • --pds-spacing-2xs
  • --pds-spacing-5xs
  • --pds-spacing-l
  • --pds-spacing-s
  • --pds-spacing-xs
  • --pds-typography-ff-code
  • --pds-typography-ff-default
  • --pds-typography-lh-m
  • --pds-typography-size-xs

No raw values. Custom variables (see custom.md): --timeline-gap, --timeline-item-status-color, --timeline-rail-color.

Impact

From impact.md: nothing reaches existing PDS components; everything is scoped to pds-timeline and pds-timeline-item. Chain of Thought builds on it (it renders Timeline and its rows are TimelineItem), and Event log with agent composes it. Changing the rename's classes (pds-agent-step to pds-timeline-item, --agent-step-* to --timeline-item-*) reached Chain of Thought, which was updated. The local Spinner and Icon wrapper are used for the active spinner and icons.

Reused vs net-new

  • Reused: Icon (local wrapper), Spinner (local, for the active status); in stories, the package's CodeBlock, Tag and FlexContainer.
  • Net-new: Timeline and TimelineItem (classes pds-timeline* and pds-timeline-item*, four custom variables, no raw values). See custom.md.

Accessibility

  • Timeline is an ol; each child is an li, so a screen reader announces the number of entries. A divider child keeps its own separator role.
  • The rail is a CSS pseudo-element, decorative, not announced.
  • An item's status is announced as visually-hidden text (labelStrings, translatable); the active spinner has its own label.
  • time renders a time element; pass timeDateTime for a machine-readable value.
  • One chevron for every disclosure (2026-10-21): an item with detail shows an angle-down that flips to up when open, the same as the Chain of Thought summary and the PDS Panel Row's expand button (it was a right chevron turning down).
  • An item with detail is a native details/summary: Enter and Space toggle it, and it takes the visible focus ring (--pds-color-interactive-focus). With a time, the whole row is the click target.
  • a11y panel: no violations on the Event log with agent story in light. Not run on the other stories, and not run in dark. Run it in both before the port.

Decisions and open questions

  • Tone. Items take isAgentic one by one. Should a TimelineGroup tint a region between two dividers? Only if the same region keeps being rebuilt.
  • Live tail. ThinkingState is a plain child today. Should TimelineItem get an active look instead?
  • Time column on narrow widths. The time is flex-shrink: 0; a long label wraps. Confirm that is right.
  • Collapsing old entries ("Show N earlier events") is not built.
  • Nesting depth. One level (a Chain of Thought inside the log) is tested; deeper is not.
  • Custom icon on a done item replaces the check, so completion shows only by color and hidden text. Keep a small check?
  • Content when an item finishes. The consumer decides whether to collapse it.
  • Divider styling. Which agentic LabeledDivider styling stays (gradient or filled) is open on that proposal.
  • Not built: a larger, bolder item style, and a Chain of Thought header with a metadata line.

Port notes

  • New components, likely src/components/ with a timeline folder holding both. Use npm run generate:component for each, then copy the files over.
  • Swap the local Icon wrapper and mergeClasses for the real ones, and port the Spinner proposal for the active status (ready: ../Spinner/HANDOFF.md). The order is in docs/PORT-ORDER.md.
  • The rail uses the lh CSS unit. Confirm it is within the browser support PDS targets.
  • Port Timeline and TimelineItem before Chain of Thought, which depends on both.
  • AgentStep never shipped, so there is nothing to migrate in pds-toolkit-react.

Preview

npm run storybook, then Component Proposals/Timeline/Timeline and Component Proposals/Timeline/Timeline Item. The composition is under Recipes/Event log with agent.

Figma: https://www.figma.com/design/AMGWJbraJoi2PKWnxo29Ah/PDS-Proposals?node-id=306-43, page Timeline (captured from Storybook, flat layers, light and dark).