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/reactFor 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.
Description
A made-up app's 2026, seen on 13 October. Shipped milestones are solid, upcoming ones hollow and the one at risk amber; the two that land a day apart share a pin with their count. Hover anywhere along the ruler and the nearest pin answers.
start and end set any span of days (here one quarter, each month still cut in four), tone colors the pins, and value with onValueChange lets your app own which milestone is picked.
Code
milestone-ruler-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propsmilestones, start, end, today, value, tone…
Props
| Prop | Type | |
|---|---|---|
milestonesRequired | readonly Milestone[] | – |
start | MilestoneDateThe ruler's first day. Default: January 1 of today's year (or of the first milestone's). | – |
end | MilestoneDateThe ruler's last day. Default: December 31 of the start's year. | – |
today | MilestoneDateMarked on the ruler, and splits done from upcoming. Default: the browser's clock once mounted (none on the server). | – |
value | string | nullThe selected milestone's id, shown when nothing is hovered or focused (controlled); | – |
defaultValue | string | nullDefault: the next milestone from today, else the last one. | – |
onValueChange | (id: string | null) => void | – |
tone | MilestoneRulerTone'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) => stringFormats a day in the callout and the spoken names. Default: the locale's short month and day. | – |
locale | stringDefault: the YoujProvider locale. | – |
label | stringAccessible 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
| Moment | Tokens | ||
|---|---|---|---|
| Risethe pins grow up from the ruler left to right when a third of it scrolls into view | duration.deliberateease.enter560ms · 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.standard140ms · 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.standard140ms · 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(yourlabel, or the labels' "Milestones") described by a summary: how many milestones, and how many are done. - Every pin is an
optionnamed 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 isaria-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.