← 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.md117 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, where the subtitle goes and how much space the list keeps around itself.

  • 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.
  • Added 2026-11-02: subtitle above, list padding, body slot. A billing-switch screen needs the date as an eyebrow above the title, flat, inside a Panel that already has padding. It was first built as a separate "Milestone Compact"; the only differences from flat Milestone were these two options, so it was folded in. subtitlePosition gains above (next to below and start) and the list gets padding: 'none' | 'standard' (default standard, as before). The body (children) is documented as a slot. Two instance stories were added to both Milestone List and Milestone: Subtitle Above and Slot (not wrapped in a Panel). The names follow PDS: above / below mirror ProgressBar's messagePosition, and padding follows the container padding scale (Panel's standard is spacing-l, the same as the list's own space).
  • Added 2026-11-02: a Badge beside the subtitle (three props). badgeLabel, badgeColor and badgeSize on Milestone render a PDS Badge on the subtitle's line, for example "You're here". It shows when badgeLabel is set, so there is no hasBadge: PDS uses a has… boolean only when the component supplies the content itself (hasCopyButton), and takes content as a prop otherwise (Button tally, SectionMessage badgeLabel). A single badge object was tried and replaced by three flat props because separate controls are easier to adjust in Storybook. It uses the published PDS Badge, not the local Badge proposal. With subtitlePosition='start' the Badge stacks under the subtitle, 4px apart and aligned to the same end.
  • 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
badgeColor BadgeProps['color'] 'info-muted' Type (color) of the Badge. Only used when badgeLabel is set.
badgeLabel BadgeProps['label'] none Content of a PDS Badge beside the subtitle. The Badge is shown when this is set.
badgeSize BadgeProps['size'] 'xs' Size of the Badge. Only used when badgeLabel is set.
children ReactNode none The whole body under the title: the description, then anything else (a table, a summary, a form).
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.
padding 'none' | 'standard' 'standard' Space above the first milestone and below the last. none for a list inside a container that already has padding (a Panel).
subtitlePosition 'above' | 'below' | 'start' 'below' above the title, below it, 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 above, below and at the start; list padding standard and none; with and without subtitle and body; the body as a slot (text, a table, a summary).

The options are also exported as value arrays (milestoneVariants, milestoneListPaddings, milestoneListSubtitlePositions), and the types are derived from them, as PDS does for tagColors, so Storybook can list them.

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-4xs
  • --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). The 2026-11-02 additions are .pds-milestone-list--padding-none and the above value; the subtitle is now a flex row with a 2xs gap so the Badge sits level with a date. See custom.md.
  • Reused: PDS Badge (published package) for the badge* props. The Badge color control lists the colors in the stories, because the package exports the type but no list.
  • Story-only: the Slot story uses PDS Panel and Table, and a small summary box that stands in for the screen's own content.

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 (axe 4.14 run on every Milestone and Milestone List story, light and dark, 2026-11-02): no violations. Where a list or a Badge is shown, one color-contrast check is inconclusive (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.
  • Narrow phone widths (375px) checked for start with a Badge. Not checked: 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 subtitlePosition and variant to its milestones through milestone-list-context.ts (a React context).
  • With subtitlePosition='start' and a Badge, the subtitle column has a zero height so the date and Badge stack hangs down into the gutter without making the title's row taller (milestone.css). It is a workaround; use a cleaner layout if the port has one.
  • milestone-story-shared.tsx and MilestoneVariantsExample.tsx are story-only; do not port them. The Badge color control lists the 33 colors by hand because the package exports the type but no list.
  • 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. Both pages have the same controls (all 13; each says which component the prop belongs to) and the same five examples: Default, Flat, Subtitle At Start, Subtitle Above and Slot. Figma: the current version is the page Milestone, https://www.figma.com/design/AMGWJbraJoi2PKWnxo29Ah/PDS-Proposals?node-id=458-8498 (captured from Storybook, flat layers, light only: Default, Flat, Subtitle At Start, Subtitle Above and Slot for both Milestone and Milestone List; Subtitle Above and Slot show a card and a flat list). On that page there is also a section Milestone component with a real Figma component, Milestone (24 variants: card or flat, default or active, subtitle below, above or at the start, with or without the Badge; properties Title, Subtitle and Body, an instance swap that stands for the whole body and defaults to a Milestone body component (a description, with or without a swappable Extra content below it; the PDS Placeholder content is a preferred alternative; the Body is an exposed instance, so Description and Extra content show on the Milestone instance); built from the PDS Panel, Badge and Placeholder content, PDS variables and the heading/m, copy/xs and copy/s text styles). A second component, Milestone List (12 variants: card or flat, subtitle below, above or at the start, padding standard or none; 3 rows with one continuous line, rows exposed so their Badge, Description and Extra content can be edited on the list), is in the same section. The original four frames (2026-10-22: Default, Flat, List, List Subtitle At Start) are kept in the section Old of the same page.

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