← All proposals

Milestone

Net-new, in two parts, in their own family so "timeline" keeps one meaning (the log).

Net-new componentOpen in Storybook

proposals/Milestone/HANDOFF.md104 lines · View on GitHub

Handoff: Milestone (with MilestoneList)

What and why

PDS has no pattern for a sequence of steps people read in order, for example the stages of an application process: a heading, a subtitle (often a date) and a short paragraph each, hung off one vertical line. Timeline is a log of one-line events with status icons and times, and cannot express it. This proposal is two components in their own family so "timeline" keeps one meaning (the log): Milestone, one step (a dot on the line, a title, a subtitle, a body, on a card or flat), and MilestoneList, the ol that owns the list semantics, the spacing, the full-height line and where the subtitle goes.

  • Figma / Jira: Figma page under Preview; Jira none. The source was two screenshots from the designer (2026-10-22): two steps, each a card with a heading, a date and a paragraph on a grey line with a ringed dot, and the same with the date at the left of the line.
  • Context: context.md

Exploration and decisions

  • Its own family, not part of Timeline. It started as TimelineMilestone plus a variant='milestone' on Timeline, and was split out the same day. Anatomy differs (a card with a heading and body, against a one-line row), and Timeline stays free of milestone-only props. Renaming the log to ActivityLog was rejected: it touches 13 files, including handoffs.
  • Marker state is one boolean, isActive (not done / current / upcoming): the milestone in focus gets the accent fill with a ring and aria-current='step'; the others are an empty circle.
  • Card: default Panel or flat only. It had a cardVariant (any Panel variant, default raised) and a hasCard boolean; both were removed. The look is now variant: 'card' | 'flat' (string union, like other PDS props), set on the list and overridable per milestone. Other Panel variants can join the union later.
  • Subtitle, not date. It cannot always be a date, so subtitle is a node the consumer formats. subtitlePosition='start' on the list moves it to the left of the line, beside the dot. It is on the list because the gutter belongs to it.
  • Accent is the interactive link color, on the dot only. The grey active ring was compared and rejected.
  • Heading level is a prop (headingLevel, default 3) because only the screen knows the page's outline.
  • Which of Chain of Thought, Timeline or Milestone List: a TL;DR guide with a three-question test is on the Usage page of all five (Chain of Thought, Timeline, Timeline Item, Milestone, Milestone List).

API

Milestone

Prop Type Default Purpose
children ReactNode none The body: text, links, anything.
className string none Additional class names.
headingLevel 2 | 3 | 4 | 5 | 6 3 Level of the title's heading element.
isActive boolean false The milestone in focus: accent dot with a ring, aria-current='step'.
subtitle ReactNode none Quiet line in the secondary color: a date, a range, a stage. Wrap a date in time for a machine-readable value.
title ReactNode required What the milestone is called.
variant 'card' | 'flat' the list's, card card puts it on the default Panel; flat leaves the text alone on the line. Overrides the list's.

MilestoneList

Prop Type Default Purpose
children ReactNode none The milestones. Each child becomes a list item.
className string none Additional class names.
subtitlePosition 'below' | 'start' 'below' below the title, or start: left of the line, beside the dot. Falls back to below under 480px.
variant 'card' | 'flat' 'card' Whether milestones are on cards or flat. A milestone can override it, through a React context.

Variants and states: card and flat; active and inactive; subtitle below and at the start; with and without subtitle and body.

Tokens used

  • --pds-border-width-default
  • --pds-border-width-thicker
  • --pds-color-border-separator
  • --pds-color-foreground-default-secondary
  • --pds-color-interactive-link-default
  • --pds-color-surface-default
  • --pds-elevation-raised
  • --pds-spacing-2xl
  • --pds-spacing-2xs
  • --pds-spacing-4xl
  • --pds-spacing-7xl
  • --pds-spacing-l
  • --pds-spacing-m
  • --pds-spacing-s
  • --pds-spacing-xs

No raw values. Custom variables (see custom.md): --milestone-accent, --milestone-dot-edge, --milestone-dot-fill, --milestone-dot-ring, --milestone-dot-shadow, --milestone-edge, --milestone-gap, --milestone-inset, --milestone-list-line-color, --milestone-marker-size, --milestone-meta-width, --milestone-pad. One raw breakpoint, 480px, is kept on purpose (see custom.md).

Impact

From impact.md: nothing reaches existing PDS components; everything is scoped to pds-milestone and pds-milestone-list. No other proposal imports or uses it. The Usage pages of Chain of Thought, Timeline and Timeline Item (docs only) now carry the shared TL;DR guide.

Reused vs net-new

  • Reused: Panel (local, default variant, padding='none') for the card.
  • Net-new: Milestone and MilestoneList (classes pds-milestone* and pds-milestone-list*, 12 custom variables, one raw breakpoint). See custom.md.

Accessibility

  • MilestoneList is an ol; each child is an li, so a screen reader announces the number of steps.
  • The title is a heading; its level is set by headingLevel.
  • The line and the dot are decorative (the dot is aria-hidden).
  • The active milestone has aria-current='step'.
  • Nothing in it is interactive, so there is no keyboard behavior or focus state of its own. Links in the body keep PDS link focus.
  • a11y panel, Milestone Default and Flat, Milestone List Default and Subtitle At Start, light and dark: no violations. Subtitle At Start has one inconclusive color-contrast check (the subtitle sits over the line's pseudo-element, so axe cannot read the background); the pair is the PDS secondary text on the default surface.
  • Not checked: narrow phone widths, long subtitles, long titles.

Decisions and open questions

  • The inactive circle and the line are faint (the same light grey). Darken both through --milestone-list-line-color?
  • Other Panel variants (raised, sunken, colored, agentic) are not built; variant can take them later.
  • 480px breakpoint for subtitlePosition='start' is a custom value, lower than the PDS 768px so the layout still shows in a narrow Storybook pane. Pick the nearest breakpoint token, or 768px if the layout may fall back earlier.
  • Priority: no project is listed for it in docs/PRIORITY.md yet, so it is later.

Port notes

  • New components, likely src/components/ with a milestone folder holding both. Use npm run generate:component for each, then copy the files over.
  • Swap the local Panel and the mergeClasses stand-in for the real ones. It depends on Panel (ready: ../Panel/HANDOFF.md).
  • The dot is placed with the lh CSS unit and a few custom variables; confirm lh is within the browser support PDS targets (Timeline's rail uses it too).
  • The MilestoneList passes variant to its milestones through milestone-list-context.ts (a React context).
  • Do not mix Timeline Items and Milestones in one list.

Preview

npm run storybook, then Component Proposals/Milestones/Milestone and Component Proposals/Milestones/Milestone List. Figma: https://www.figma.com/design/AMGWJbraJoi2PKWnxo29Ah/PDS-Proposals?node-id=458-8498, page Milestone (captured from Storybook, flat layers, light only: Default, Flat, List, List Subtitle At Start).

Figma file: https://www.figma.com/design/AMGWJbraJoi2PKWnxo29Ah/PDS-Proposals?node-id=458-8498