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
TimelineMilestoneplus avariant='milestone'onTimeline, 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 toActivityLogwas 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 andaria-current='step'; the others are an empty circle. - Card: default Panel or flat only. It had a
cardVariant(any Panel variant, defaultraised) and ahasCardboolean; both were removed. The look is nowvariant: '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
subtitleis 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:
MilestoneandMilestoneList(classespds-milestone*andpds-milestone-list*, 12 custom variables, one raw breakpoint). Seecustom.md.
Accessibility
MilestoneListis anol; each child is anli, 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;
variantcan 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.mdyet, so it islater.
Port notes
- New components, likely
src/components/with amilestonefolder holding both. Usenpm run generate:componentfor each, then copy the files over. - Swap the local
Paneland themergeClassesstand-in for the real ones. It depends on Panel (ready:../Panel/HANDOFF.md). - The dot is placed with the
lhCSS unit and a few custom variables; confirmlhis within the browser support PDS targets (Timeline's rail uses it too). - The
MilestoneListpassesvariantto its milestones throughmilestone-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).