Milestones

A point in time on a lane - a handover, a release, a deadline - drawn as a diamond above the work, reached by the keys and named in the tooltip.

import { useSchedule } from "@umriss-ui/schedule";

A milestone is data beside the work: one list, each point naming its lane and the instant it stands at ({ id, lane, at, label? }). It has no duration, no task and no tone; it is not dragged and reports no intent, and work beside it is never a finding.

The keys walk to it in time order among the subtasks of its lane, the tooltip names it by its label and time, and the live region reads its lane, label and time. It is named by its label, or its id without one.

Examples

Mark a handover and a release

A point in time on a lane is a mark, not a bar: Milestones draws a diamond at at, above the work.

import { Stack } from "@umriss-ui/core";
import { useSchedule } from "@umriss-ui/schedule";
import type { Milestone, Subtask, Task } from "@umriss-ui/schedule";

/* A milestone is data beside the work: one list, each point naming its lane
   and the instant it stands at, as a release calendar holds it. It is drawn
   above the bars - the design handover below stands on the end of the design
   bar and is still seen - and it has no duration, no task and no colour of
   its own.

   Hover it and the tooltip names it and says when; the keys reach it as they
   reach the work, with the arrows in time order along the lane, and the live
   region reads its lane, its label and its time. It is not dragged and
   reports no intent. */

const day = (d: number, hours = 0) => new Date(2026, 2, 16 + d, hours).getTime();

const PROJECTS: readonly Task[] = [
  { id: "portal", name: "Customer portal", color: "light-dark(#2563eb, #6b9bff)" },
  { id: "billing", name: "Billing rework", color: "light-dark(#0d9488, #3cc7b8)" },
];

const WORK: readonly Subtask[] = [
  { id: "portal-design", task: "portal", lane: "anna", from: day(0, 9), to: day(1, 17), name: "Design" },
  { id: "portal-front", task: "portal", lane: "chloe", from: day(1, 9), to: day(3, 17), name: "Front end" },
  { id: "billing-api", task: "billing", lane: "ben", from: day(0, 9), to: day(2, 17), name: "Invoice API" },
];

const MILESTONES: readonly Milestone[] = [
  { id: "design-handover", lane: "anna", at: day(1, 17), label: "Design handover" },
  { id: "api-freeze", lane: "ben", at: day(2, 17), label: "API freeze" },
  { id: "release", lane: "chloe", at: day(4, 12), label: "Release 2.0" },
];

export default function HandoversAndReleases() {
  const { Schedule, Lane, Subtasks, Milestones } = useSchedule({ initialView: { domain: [day(0, 6), day(5, 0)] } });
  return (
    <Stack gap={3}>
      <Schedule ariaLabel="Portal and billing plan, week of 16 March" height={188}>
        <Lane id="anna" label="Anna Berg" />
        <Lane id="ben" label="Ben Ortiz" />
        <Lane id="chloe" label="Chloe Lind" />
        <Subtasks data={WORK} tasks={PROJECTS} />
        <Milestones data={MILESTONES} />
      </Schedule>
    </Stack>
  );
}

Say what a milestone is in your own tooltip

Over a milestone the target is milestone: the point itself, with its label and at, for a tooltip of your own.

import { Stack, Text } from "@umriss-ui/core";
import { useSchedule } from "@umriss-ui/schedule";
import type { Milestone, ScheduleTooltipTarget, Subtask, Task } from "@umriss-ui/schedule";

const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();

const PROJECTS: readonly Task[] = [{ id: "portal", name: "Member portal", color: "light-dark(#2563eb, #6b9bff)" }];

const WORK: readonly Subtask[] = [
  { id: "build", task: "portal", lane: "arjun", from: at(7), to: at(12), name: "Build" },
  { id: "test", task: "portal", lane: "eva", from: at(12, 30), to: at(15), name: "Test" },
];

const MILESTONES: readonly Milestone[] = [{ id: "code-freeze", lane: "arjun", at: at(12), label: "Code freeze" }];

const time = (instant: number) => new Date(instant).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });

function Own({ target }: { target: ScheduleTooltipTarget }) {
  if (target.kind === "dependency") return null;
  if (target.kind === "subtask") return <Text size="sm">{target.subtask.name}</Text>;
  return (
    <Stack gap={1}>
      <Text size="sm" weight="semibold">
        {target.milestone.label ?? target.milestone.id}
      </Text>
      <Text size="xs" tone="secondary">
        Test may begin from {time(target.milestone.at)}
      </Text>
    </Stack>
  );
}

export default function SayWhatAMilestoneIs() {
  const { Schedule, Lane, Subtasks, Milestones } = useSchedule({ initialView: { domain: [at(6, 30), at(15, 30)] } });
  return (
    <Schedule ariaLabel="Build and test around the code freeze" height={150} tooltip={(target) => <Own target={target} />}>
      <Lane id="arjun" label="Arjun Mehta" />
      <Lane id="eva" label="Eva Novak" />
      <Subtasks data={WORK} tasks={PROJECTS} />
      <Milestones data={MILESTONES} />
    </Schedule>
  );
}

When to use something else

Keyboard

The keys of First schedule apply here.

API

MilestonesProps

NameTypeDefaultDescription
datarequiredreadonly Milestone[]—The milestones of the lanes, as the caller holds them: one list, each naming its lane and the instant it stands at.

Shown in: Mark a handover and a release, Say what a milestone is in your own tooltip

Milestone

NameTypeDefaultDescription
idrequiredstring—The caller's identity of the milestone.

Shown in: Mark a handover and a release, Say what a milestone is in your own tooltip

lanerequiredstring—The id of the lane it stands on.

Shown in: Mark a handover and a release, Say what a milestone is in your own tooltip

atrequirednumber—The instant it stands at.

Shown in: Mark a handover and a release, Say what a milestone is in your own tooltip

labelstring—What it is called, where the schedule names it; without it, its id.

Shown in: Mark a handover and a release, Say what a milestone is in your own tooltip

Every page of @umriss-ui/schedule

Getting started

Plan

Time

Reading

Editing

Measuring

API index