Skip to content

Milestone Ruler

Put a year of milestones on a ruler of fine ticks, with today marked and a callout for the one you point at.

import { Milestone, MilestoneRuler } from '@youj/react'

Installation & usage

npm i @youj/react

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Use a milestone ruler for when things land across a long span: a product's year of releases, a quarter's launch plan, a grant's reporting dates. It answers "what's next, what slipped, how far along are we" at a glance and keeps one line of detail a hover away. For the order of steps in one flow use a Stage Rail; for events and durations within one incident, a Severity Timeline. The Roadmap timeline card is built on it.

import { MilestoneRuler, type Milestone } from '@youj/react'

const milestones: Milestone[] = [
  { id: 'v1-5', date: '2026-06-04', title: 'Version 1.5', description: 'New editor', major: true },
  { id: 'sso', date: '2026-10-28', title: 'SSO and audit log', status: 'at-risk' },
  { id: 'v2', date: '2026-11-26', title: 'Version 2', description: 'Rebuilt sync engine', major: true },
]

<MilestoneRuler milestones={milestones} today={today} label="Roadmap 2026" />

A milestone's date is a day: a date-only string such as "2026-11-26" is that calendar day in every time zone, and a Date or a time in ms counts by its local date. Without start and end the ruler spans January 1 to December 31 of today's year. status is done, upcoming or at-risk; left out, it is done before today and upcoming from it on. Pass today from your server so the page hydrates with the same marks; without it the ruler waits for the browser's clock. major draws a taller pin.

Milestones closer than about ten pixels share one pin with their count, so the grouping follows the space the ruler has. A picked milestone (value, or the next one from today by default) keeps its callout shown; hovering or focusing another shows that one until the pointer or focus leaves. formatDate writes the days your way.

Reference

Propsmilestones, start, end, today, value, tone…

Props

MilestoneRuler props
PropType
milestonesRequiredreadonly Milestone[]–
startMilestoneDate

The ruler's first day. Default: January 1 of today's year (or of the first milestone's).

–
endMilestoneDate

The ruler's last day. Default: December 31 of the start's year.

–
todayMilestoneDate

Marked on the ruler, and splits done from upcoming. Default: the browser's clock once mounted (none on the server).

–
valuestring | null

The selected milestone's id, shown when nothing is hovered or focused (controlled); null for none.

–
defaultValuestring | null

Default: the next milestone from today, else the last one.

–
onValueChange(id: string | null) => void–
toneMilestoneRulerTone
'accent''neutral''success''warning''danger''chart-1''chart-2''chart-3''chart-4''chart-5'

The pins' color. At-risk milestones take the warning tone.

default 'accent'

'accent'
formatDate(date: Date) => string

Formats a day in the callout and the spoken names. Default: the locale's short month and day.

–
localestring

Default: the YoujProvider locale.

–
labelstring

Accessible name. Default: the labels' "Milestones".

–
Other props pass through: extends ComponentProps<'div'>.
StylingParts and data attributes

Styling

Parts: milestone-ruler, milestone-ruler-callout-zone, milestone-ruler-callout, milestone-ruler-caret, milestone-ruler-plot, milestone-ruler-ticks, milestone-ruler-tick, milestone-ruler-today, milestone-ruler-pins, milestone-ruler-pin, milestone-ruler-dot, milestone-ruler-count, milestone-ruler-axis, milestone-ruler-month and milestone-ruler-empty. A pin has data-status (done, upcoming or at-risk), data-yj-major, data-yj-shown while its callout is up and data-yj-count when it holds several milestones; a tick has data-yj-major at a month's start. The pins' color is --milestone-ruler-tone, and the count's ink --milestone-ruler-ink.

MotionRise, callout, stretch · reduced motion

Motion

Motion tokens
MomentTokens
Risethe pins grow up from the ruler left to right when a third of it scrolls into view
duration.deliberateease.enter

560ms · cubic-bezier(0.16, 1, 0.3, 1)

560mscubic-bezier(0.16, 1, 0.3, 1)
Calloutthe callout glides to the pin under the pointer or focus, its caret staying on the pin
duration.fastease.standard

140ms · cubic-bezier(0.2, 0, 0, 1)

140mscubic-bezier(0.2, 0, 0, 1)
Stretchthe ticks within about 40px of the pointer stretch toward it, most at the pointer
duration.fastease.standard

140ms · cubic-bezier(0.2, 0, 0, 1)

140mscubic-bezier(0.2, 0, 0, 1)

Reduced motion

Drawn in place: the pins stand, the callout jumps and the ticks don't stretch.

Reduced motion

The pins are drawn standing, the callout moves at once and the ticks keep their length.

AccessibilityKeyboard, focus, screen readers

Accessibility

  • A horizontal listbox (your label, or the labels' "Milestones") described by a summary: how many milestones, and how many are done.
  • Every pin is an option named with its title, day and status ("SSO and audit log, Oct 28, At risk"), and described by its one-line description; a shared pin names each of its milestones. The picked one is aria-selected.
  • One tab stop. Left and Right walk the pins (mirrored in RTL), Home and End go to the ends, and selection follows focus. A click or tap anywhere on the ruler picks the nearest pin.
  • The callout, ticks, today mark and month names are decorative; the options say the same in words. Status is a word and a solid, hollow or warning-toned dot, never color alone.
  • Its built-in text comes from the labels, so <YoujProvider labels> translates it.