Editorial

Milestones

Roadmap or company history as an ordered list on Grid, in grid or rows layout. Rules can be off, a top border per item, or one fading horizon line with dots; the current milestone's dot is filled.

Free, MIT licenseOpen in Browse
import type { ComponentProps } from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { cva } from "class-variance-authority"
import { cn } from "cn"

import { Grid, type GridProps } from "@/components/editorial/grid"

// Milestones is a Grid rendered as an <ol>: grid uses its columns, rows is one
// column. rule owns every rule: none, top (a border on each milestone) or
// horizon (one line across the top of the list; when milestones wrap to a
// second row, that row gets markers with no line).
const milestonesVariants = cva("group/milestones relative", {
  variants: {
    variant: {
      grid: "",
      rows: "",
    },
    rule: {
      none: "",
      // The horizon: brightest mid-row, spent toward the end.
      horizon:
        "before:absolute before:inset-x-0 before:top-1 before:hidden before:h-px before:bg-linear-to-r before:from-foreground/15 before:via-foreground/30 before:to-transparent",
      top: "",
    },
  },
  defaultVariants: { variant: "grid", rule: "none" },
})

// The horizon appears once the list reaches its full column count, measured
// on the list's own space. These mirror Grid's full-count steps; keep them in
// step if Grid's change.
const HORIZON_AT = {
  1: "before:block",
  2: "@2xl:before:block",
  3: "@3xl:before:block",
  4: "@3xl:before:block",
  5: "@3xl:before:block",
  6: "@5xl:before:block",
} as const

// horizon is one line across a row, so it belongs to grid only.
type MilestonesProps = ComponentProps<"ol"> & {
  /**
   * The most columns a `grid` list uses, 4 by default. They step up as the
   * list's space widens, the way `Grid`'s do. A `rows` list is always one
   * column.
   */
  cols?: GridProps["cols"]
} & (
    | {
        /**
         * `grid` sets the milestones in columns. `rows` stacks them one per
         * row.
         */
        variant?: "grid"
        /**
         * The lines drawn on the list. `top` puts a border above each
         * milestone. `horizon`, for `grid` only, draws one line across the
         * top of the list once all its columns show; a second row of
         * milestones gets no line.
         */
        rule?: "none" | "horizon" | "top"
      }
    | {
        /**
         * `grid` sets the milestones in columns. `rows` stacks them one per
         * row.
         */
        variant: "rows"
        /**
         * The lines drawn on the list. `top` puts a border above each
         * milestone. `horizon`, for `grid` only, draws one line across the
         * top of the list once all its columns show; a second row of
         * milestones gets no line.
         */
        rule?: "none" | "top"
      }
  )

/**
 * A roadmap or timeline, rendered as an ordered list. Put a `Milestone`
 * inside for each step.
 *
 * `grid` (the default) sets the milestones in up to `cols` columns, four
 * unless you set it. `rows` stacks them one per row. `className` lands on the
 * `<ol>`, but its columns follow the width of a wrapper around it, so to
 * narrow the list, size its parent.
 */
function Milestones({
  className,
  variant = "grid",
  cols = 4,
  rule = "none",
  ...props
}: MilestonesProps) {
  const grid = variant === "grid"
  return (
    <Grid
      data-slot="milestones"
      data-variant={variant}
      data-rule={rule}
      role="list"
      render={<ol />}
      cols={grid ? cols : 1}
      gap={grid ? "lg" : "none"}
      className={cn(
        milestonesVariants({ variant, rule }),
        grid && rule === "horizon" && HORIZON_AT[cols ?? 4],
        className
      )}
      // Grid's props are typed for its default div; this list renders an <ol>.
      {...(props as Omit<GridProps, "cols" | "gap">)}
    />
  )
}

/**
 * One step inside `Milestones`, rendered as an `<li>`. Its text is muted
 * unless it is the current step.
 *
 * In a `grid` list it usually holds a `MilestoneMarker` and a
 * `MilestoneContent`, with `MilestoneMeta` inside the content. In a `rows`
 * list, put `MilestoneMeta` and `MilestoneContent` side by side: once the
 * list is 24rem wide, the meta takes a narrow first column.
 */
function Milestone({
  className,
  current,
  render,
  ...props
}: useRender.ComponentProps<"li", { slot: "milestone"; current: boolean }> & {
  /**
   * Marks this as the current step: its text turns full strength, its
   * `MilestoneMarker` fills in, and screen readers announce it as the
   * current step.
   */
  current?: boolean
}) {
  return useRender({
    defaultTagName: "li",
    render,
    state: { slot: "milestone", current: Boolean(current) },
    props: mergeProps<"li">(
      {
        "aria-current": current ? "step" : undefined,
        className: cn(
          "group/milestone relative flex flex-col gap-6 text-muted-foreground data-current:text-foreground",
          "group-data-[rule=top]/milestones:border-t group-data-[rule=top]/milestones:border-border group-data-[rule=top]/milestones:pt-6",
          "group-data-[variant=rows]/milestones:grid group-data-[variant=rows]/milestones:grid-cols-1 group-data-[variant=rows]/milestones:gap-2 group-data-[variant=rows]/milestones:py-6 group-data-[variant=rows]/milestones:@sm:grid-cols-4 group-data-[variant=rows]/milestones:@sm:gap-8",
          className
        ),
      },
      props
    ),
  })
}

/**
 * The dot on the `horizon` line. Place it first in `Milestone` so it sits on
 * that line. It shows as an open ring, filled in on the current milestone,
 * and is hidden from screen readers.
 */
function MilestoneMarker({ className, ...props }: ComponentProps<"span">) {
  return (
    <span
      data-slot="milestone-marker"
      aria-hidden="true"
      className={cn(
        "size-2 rounded-full bg-background ring-1 ring-foreground/30 group-data-current/milestone:bg-foreground group-data-current/milestone:ring-0",
        className
      )}
      {...props}
    />
  )
}

/**
 * A short line of details for a milestone, such as its `MilestoneDate` and a
 * `MilestoneLabel`, side by side. In a `rows` list they stack once the list
 * is 24rem wide.
 */
function MilestoneMeta({ className, ...props }: ComponentProps<"p">) {
  return (
    <p
      data-slot="milestone-meta"
      className={cn(
        "flex gap-x-3 text-sm/7 group-data-[variant=rows]/milestones:@sm:flex-col group-data-[variant=rows]/milestones:@sm:gap-0",
        className
      )}
      {...props}
    />
  )
}

/**
 * The milestone's date, as a `<time>`. Give it a `dateTime` value that
 * machines can read.
 */
function MilestoneDate({ className, ...props }: ComponentProps<"time">) {
  return <time data-slot="milestone-date" className={className} {...props} />
}

/**
 * A short label in `MilestoneMeta`, such as a status or a version number, in
 * medium-weight, full-strength text.
 */
function MilestoneLabel({ className, ...props }: ComponentProps<"span">) {
  return (
    <span
      data-slot="milestone-label"
      className={cn("font-medium text-foreground", className)}
      {...props}
    />
  )
}

/**
 * The body of a milestone: put a `MilestoneTitle` and a
 * `MilestoneDescription` inside. In a `rows` list it spans the three columns
 * beside `MilestoneMeta` once the list is 24rem wide.
 */
function MilestoneContent({ className, ...props }: ComponentProps<"div">) {
  return (
    <div
      data-slot="milestone-content"
      className={cn(
        "grid min-w-0 content-start gap-2 group-data-[variant=rows]/milestones:@sm:col-span-3",
        className
      )}
      {...props}
    />
  )
}

type MilestoneTitleState = { slot: "milestone-title" }

/**
 * The milestone's heading, an `<h3>` in the display font by default. Pass
 * `render` to use another heading level.
 */
function MilestoneTitle({
  className,
  render,
  ...props
}: useRender.ComponentProps<"h3", MilestoneTitleState>) {
  return useRender({
    defaultTagName: "h3",
    render,
    state: { slot: "milestone-title" },
    props: mergeProps<"h3">(
      {
        className: cn(
          "font-display text-2xl/8 tracking-tight text-foreground",
          className
        ),
      },
      props
    ),
  })
}

/** A paragraph that describes the milestone, in muted text. */
function MilestoneDescription({ className, ...props }: ComponentProps<"p">) {
  return (
    <p
      data-slot="milestone-description"
      className={cn("text-base/7 text-pretty text-muted-foreground", className)}
      {...props}
    />
  )
}

export {
  Milestones,
  Milestone,
  MilestoneMarker,
  MilestoneMeta,
  MilestoneDate,
  MilestoneLabel,
  MilestoneContent,
  MilestoneTitle,
  MilestoneDescription,
}

Installation

pnpm dlx shadcn add @01orbital/milestones
npx shadcn add @01orbital/milestones
yarn shadcn add @01orbital/milestones
bunx --bun shadcn add @01orbital/milestones

Free under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.

It lands at components/editorial/milestones.tsx, as source you own and edit.

The example also uses section. Install it too:

pnpm dlx shadcn add @01orbital/section
npx shadcn add @01orbital/section
yarn shadcn add @01orbital/section
bunx --bun shadcn add @01orbital/section

Usage

import {
  Milestone,
  MilestoneContent,
  MilestoneDate,
  MilestoneDescription,
  MilestoneLabel,
  MilestoneMarker,
  MilestoneMeta,
  Milestones,
  MilestoneTitle,
} from "@/components/editorial/milestones"
import {
  Section,
  SectionContent,
} from "@/components/editorial/section"

const COPY = {
  label: "Now",
  items: [
    {
      date: "2026-06",
      dateLabel: "June",
      title: "One registry",
      body: "Every block from one install.",
    },
    {
      date: "2026-09",
      dateLabel: "September",
      title: "Parts",
      body: "Blocks compose from named parts.",
      current: true,
    },
    {
      date: "2026-11",
      dateLabel: "November",
      title: "Registers",
      body: "Palettes layer over one theme.",
    },
  ],
} as const

export default function MilestonesDemo() {
  return (
    <Section>
      <SectionContent className="items-center">
        <div className="w-full max-w-4xl">
          <Milestones cols={3} rule="horizon">
            {COPY.items.map((m) => (
              <Milestone key={m.title} current={"current" in m}>
                <MilestoneMarker />
                <MilestoneContent>
                  <MilestoneMeta>
                    <MilestoneDate dateTime={m.date}>
                      {m.dateLabel}
                    </MilestoneDate>
                    {"current" in m ? (
                      <MilestoneLabel>{COPY.label}</MilestoneLabel>
                    ) : null}
                  </MilestoneMeta>
                  <MilestoneTitle>{m.title}</MilestoneTitle>
                  <MilestoneDescription className="text-sm/7">
                    {m.body}
                  </MilestoneDescription>
                </MilestoneContent>
              </Milestone>
            ))}
          </Milestones>
        </div>
      </SectionContent>
    </Section>
  )
}

Anatomy

Milestones is built from 9 parts. In the example they nest like this:

Milestones
└─ Milestone
   ├─ MilestoneMarker
   └─ MilestoneContent
      ├─ MilestoneMeta
      │  ├─ MilestoneDate
      │  └─ MilestoneLabel
      ├─ MilestoneTitle
      └─ MilestoneDescription
  • Milestones: A roadmap or timeline, rendered as an ordered list. Put a Milestone inside for each step.
  • Milestone: One step inside Milestones, rendered as an <li>. Its text is muted unless it is the current step.
  • MilestoneMarker: The dot on the horizon line. Place it first in Milestone so it sits on that line. It shows as an open ring, filled in on the current milestone, and is hidden from screen readers.
  • MilestoneContent: The body of a milestone: put a MilestoneTitle and a MilestoneDescription inside. In a rows list it spans the three columns beside MilestoneMeta once the list is 24rem wide.
  • MilestoneMeta: A short line of details for a milestone, such as its MilestoneDate and a MilestoneLabel, side by side. In a rows list they stack once the list is 24rem wide.
  • MilestoneDate: The milestone's date, as a <time>. Give it a dateTime value that machines can read.
  • MilestoneLabel: A short label in MilestoneMeta, such as a status or a version number, in medium-weight, full-strength text.
  • MilestoneTitle: The milestone's heading, an <h3> in the display font by default. Pass render to use another heading level.
  • MilestoneDescription: A paragraph that describes the milestone, in muted text.

API reference

Milestones

A roadmap or timeline, rendered as an ordered list. Put a Milestone inside for each step.

grid (the default) sets the milestones in up to cols columns, four unless you set it. rows stacks them one per row. className lands on the <ol>, but its columns follow the width of a wrapper around it, so to narrow the list, size its parent.

Prop

Type

It also accepts the HTML attributes of the element it renders.

Milestone

One step inside Milestones, rendered as an <li>. Its text is muted unless it is the current step.

In a grid list it usually holds a MilestoneMarker and a MilestoneContent, with MilestoneMeta inside the content. In a rows list, put MilestoneMeta and MilestoneContent side by side: once the list is 24rem wide, the meta takes a narrow first column.

Prop

Type

It also accepts the props of the Base UI part it wraps, and the HTML attributes of the element it renders.

MilestoneMarker

The dot on the horizon line. Place it first in Milestone so it sits on that line. It shows as an open ring, filled in on the current milestone, and is hidden from screen readers.

It accepts the HTML attributes of the element it renders.

MilestoneContent

The body of a milestone: put a MilestoneTitle and a MilestoneDescription inside. In a rows list it spans the three columns beside MilestoneMeta once the list is 24rem wide.

It accepts the HTML attributes of the element it renders.

MilestoneMeta

A short line of details for a milestone, such as its MilestoneDate and a MilestoneLabel, side by side. In a rows list they stack once the list is 24rem wide.

It accepts the HTML attributes of the element it renders.

MilestoneDate

The milestone's date, as a <time>. Give it a dateTime value that machines can read.

It accepts the HTML attributes of the element it renders.

MilestoneLabel

A short label in MilestoneMeta, such as a status or a version number, in medium-weight, full-strength text.

It accepts the HTML attributes of the element it renders.

MilestoneTitle

The milestone's heading, an <h3> in the display font by default. Pass render to use another heading level.

It accepts the props of the Base UI part it wraps, and the HTML attributes of the element it renders.

MilestoneDescription

A paragraph that describes the milestone, in muted text.

It accepts the HTML attributes of the element it renders.

Dependencies

npm: @base-ui/react, class-variance-authority, cn. The CLI installs them for you.

Registry pieces it installs with it:

  • font-hanken-grotesk
  • grid