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
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. - 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.
subtitlePositiongainsabove(next tobelowandstart) and the list getspadding: 'none' | 'standard'(defaultstandard, 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/belowmirror ProgressBar'smessagePosition, andpaddingfollows the container padding scale (Panel'sstandardisspacing-l, the same as the list's own space). - Added 2026-11-02: a Badge beside the subtitle (three props).
badgeLabel,badgeColorandbadgeSizeonMilestonerender a PDS Badge on the subtitle's line, for example "You're here". It shows whenbadgeLabelis set, so there is nohasBadge: PDS uses ahas…boolean only when the component supplies the content itself (hasCopyButton), and takes content as a prop otherwise (Buttontally,SectionMessagebadgeLabel). A singlebadgeobject was tried and replaced by three flat props because separate controls are easier to adjust in Storybook. It uses the published PDSBadge, not the local Badge proposal. WithsubtitlePosition='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:
MilestoneandMilestoneList(classespds-milestone*andpds-milestone-list*, 12 custom variables, one raw breakpoint). The 2026-11-02 additions are.pds-milestone-list--padding-noneand theabovevalue; the subtitle is now a flex row with a2xsgap so the Badge sits level with a date. Seecustom.md. - Reused: PDS
Badge(published package) for thebadge*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
PanelandTable, and a small summary box that stands in for the screen's own content.
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 (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
startwith 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;
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
MilestoneListpassessubtitlePositionandvariantto its milestones throughmilestone-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.tsxandMilestoneVariantsExample.tsxare 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.