Tooltip

Names what the pointer rests on - a subtask with its times and findings, a dependency with its lag - without a click (also called a hover card). Replace its content when your application knows more than the schedule.

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

Its words come from the wording of @umriss-ui/core and its numbers from its formats, so a provider switches it with everything else. It steps aside while a drag is in flight and is placed where it has room.

The keyboard's active subtask shows the same tooltip as the pointer.

Examples

Read the default tooltip

Rest the pointer on a leg: the tooltip names the consignment, its times, loading and unloading, and every finding on it.

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

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

const VEHICLES = [
  { id: "van-402", label: "Van FP 402 R" },
  { id: "truck-520", label: "Truck FP 520 E" },
  { id: "van-455", label: "E-van FP 455 R" },
];

const CONSIGNMENTS: readonly Task[] = [
  { id: "c-2041", name: "C-2041 Holloway Garden Supplies", color: "light-dark(#2563eb, #6b9bff)" },
  { id: "c-2043", name: "C-2043 Oakridge Pharmacy", color: "light-dark(#c2410c, #f08a52)" },
];

/* Two legs share the van from 10:00 - an overlap - and the transfer onto the
   e-van at 11:30 is fifteen minutes short. */
const LEGS: readonly Subtask[] = [
  { id: "c-2041-2", task: "c-2041", lane: "van-402", from: at(8), to: at(10, 30), leadIn: min(30), leadOut: min(15) },
  { id: "c-2043-1", task: "c-2043", lane: "truck-520", from: at(6, 30), to: at(8), leadIn: min(30), leadOut: min(15) },
  { id: "c-2043-2", task: "c-2043", lane: "van-402", from: at(10), to: at(11, 30), leadIn: min(15) },
  { id: "c-2043-3", task: "c-2043", lane: "van-455", from: at(12), to: at(14), leadIn: min(20), leadOut: min(20) },
];

const TRANSFERS: readonly Dependency[] = [
  { id: "t-2043-1", from: "c-2043-1", to: "c-2043-2", lag: min(45) },
  { id: "t-2043-2", from: "c-2043-2", to: "c-2043-3", lag: min(25) },
];

export default function DefaultTooltip() {
  const { Schedule, Lane, Subtasks, Dependencies } = useSchedule({ initialView: { domain: [at(5, 30), at(18)] } });
  return (
    <Schedule ariaLabel="Three vehicles with the default tooltip" height={196}>
      {VEHICLES.map((vehicle) => (
        <Lane key={vehicle.id} id={vehicle.id} label={vehicle.label} />
      ))}
      <Dependencies data={TRANSFERS} />
      <Subtasks data={LEGS} tasks={CONSIGNMENTS} />
    </Schedule>
  );
}

Write your own tooltip content

Pass tooltip a function to show what only your application knows - here the load - with the findings the schedule found.

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

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

const VEHICLES = [
  { id: "van-402", label: "Van FP 402 R" },
  { id: "truck-520", label: "Truck FP 520 E" },
  { id: "van-455", label: "E-van FP 455 R" },
];

const CONSIGNMENTS: readonly Task[] = [
  { id: "c-2041", name: "C-2041 Holloway Garden Supplies", color: "light-dark(#2563eb, #6b9bff)" },
  { id: "c-2043", name: "C-2043 Oakridge Pharmacy", color: "light-dark(#c2410c, #f08a52)" },
];

const LEGS: readonly Subtask[] = [
  { id: "c-2041-2", task: "c-2041", lane: "van-402", from: at(8), to: at(10, 30), leadIn: min(30), leadOut: min(15) },
  { id: "c-2043-1", task: "c-2043", lane: "truck-520", from: at(6, 30), to: at(8), leadIn: min(30), leadOut: min(15) },
  { id: "c-2043-2", task: "c-2043", lane: "van-402", from: at(10), to: at(11, 30), leadIn: min(15) },
  { id: "c-2043-3", task: "c-2043", lane: "van-455", from: at(12), to: at(14), leadIn: min(20), leadOut: min(20) },
];

const TRANSFERS: readonly Dependency[] = [
  { id: "t-2043-1", from: "c-2043-1", to: "c-2043-2", lag: min(45) },
  { id: "t-2043-2", from: "c-2043-2", to: "c-2043-3", lag: min(25) },
];

/* What the application knows and the schedule does not. */
const LOADS: Record<string, string> = {
  "c-2041": "6 pallets · 1,140 kg",
  "c-2043": "2 pallets · 420 kg, chilled",
};

function LoadTooltip({ target }: { target: ScheduleTooltipTarget }) {
  const task = target.kind === "milestone" ? undefined : target.task;
  const found =
    target.kind === "subtask" ? target.overlapping.length + target.violatedDependencies.length : target.kind === "dependency" && target.violated ? 1 : 0;
  return (
    <Stack gap={1}>
      <Text size="sm" weight="semibold">
        {task?.name ?? "Unknown consignment"}
      </Text>
      <Text size="xs" tone="secondary">
        {task ? LOADS[task.id] : ""}
      </Text>
      {found > 0 && <Badge tone="danger">{found === 1 ? "1 finding" : `${found} findings`}</Badge>}
    </Stack>
  );
}

export default function OwnTooltip() {
  const { Schedule, Lane, Subtasks, Dependencies } = useSchedule({ initialView: { domain: [at(5, 30), at(18)] } });
  return (
    <Schedule
      ariaLabel="Three vehicles with the load in the tooltip"
      height={196}
      tooltip={(target) => <LoadTooltip target={target} />}
    >
      {VEHICLES.map((vehicle) => (
        <Lane key={vehicle.id} id={vehicle.id} label={vehicle.label} />
      ))}
      <Dependencies data={TRANSFERS} />
      <Subtasks data={LEGS} tasks={CONSIGNMENTS} />
    </Schedule>
  );
}

Show a detail panel instead

Set tooltip={false} when a panel of your own shows the item under the pointer, filled from onInteraction.

import { useState } from "react";
import { Badge, Card, CardBody, Stack, Text } from "@umriss-ui/core";
import { useSchedule } from "@umriss-ui/schedule";
import type { ScheduleInteraction } from "@umriss-ui/schedule";

/* Data from the planning world, written out here so the example runs on its own. */

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

interface Person {
  id: string;
  name: string;
  role: "Developer" | "Designer" | "Product manager" | "QA engineer";
  team: "Web" | "Apps";
  /** Hours a week they can be planned for. */
  capacity: number;
}

const PEOPLE: readonly Person[] = [
  { id: "maya", name: "Maya Lindgren", role: "Product manager", team: "Web", capacity: 32 },
  { id: "arjun", name: "Arjun Mehta", role: "Developer", team: "Web", capacity: 40 },
  { id: "chloe", name: "Chloe Durand", role: "Developer", team: "Web", capacity: 40 },
  { id: "noah", name: "Noah Fischer", role: "Designer", team: "Web", capacity: 24 },
  { id: "eva", name: "Eva Novak", role: "QA engineer", team: "Web", capacity: 40 },
  { id: "luis", name: "Luis Moreno", role: "Product manager", team: "Apps", capacity: 40 },
  { id: "hana", name: "Hana Sato", role: "Developer", team: "Apps", capacity: 40 },
  { id: "kofi", name: "Kofi Mensah", role: "Developer", team: "Apps", capacity: 32 },
  { id: "freya", name: "Freya Olsen", role: "Designer", team: "Apps", capacity: 40 },
  { id: "david", name: "David Kowalski", role: "QA engineer", team: "Apps", capacity: 20 },
];

interface Project {
  id: string;
  name: string;
  client: string;
  color: string;
}

const PROJECTS: readonly Project[] = [
  { id: "shop", name: "Online shop relaunch", client: "Pembury Home", color: "light-dark(#2563eb, #6b9bff)" },
  { id: "booking", name: "Booking app", client: "Saltmarsh Clinics", color: "light-dark(#0d9488, #3cc7b8)" },
  { id: "portal", name: "Member portal", client: "Rowan Credit Union", color: "light-dark(#7c3aed, #a98bfa)" },
  { id: "intranet", name: "Intranet", client: "Tidewell (internal)", color: "light-dark(#c2410c, #f08a52)" },
];

/** A work item, shaped as `@umriss-ui/schedule`'s `Subtask`. */
interface WorkItem {
  id: string;
  /** The project. */
  task: string;
  /** The person. */
  lane: string;
  from: number;
  to: number;
  name: string;
  sprint: string;
  /** Hours, as estimated at planning. */
  estimate: number;
  status: "to do" | "in progress" | "in review" | "done";
}

const item = (id: string, task: string, lane: string, from: number, to: number, name: string, estimate: number, status: WorkItem["status"]): WorkItem =>
  ({ id, task, lane, from, to, name, sprint: "sprint-14", estimate, status });

const WORK: readonly WorkItem[] = [
  item("w-101", "portal", "arjun", at(9), at(11, 17), "Sign-in with e-mail code", 18, "done"),
  item("w-102", "portal", "arjun", at(12), at(17, 17), "Profile page", 26, "in progress"),
  item("w-103", "portal", "chloe", at(9), at(10, 17), "Session handling", 12, "done"),
  item("w-104", "shop", "chloe", at(11), at(13, 17), "Basket keeps items across devices", 20, "in review"),
  item("w-105", "portal", "chloe", at(16), at(19, 17), "Change of address form", 24, "in progress"),
  item("w-106", "portal", "noah", at(9), at(12, 13), "Profile page design", 14, "done"),
  item("w-107", "shop", "noah", at(16), at(18, 17), "Search results layout", 12, "to do"),
  item("w-108", "portal", "eva", at(12), at(13, 17), "Test plan for sign-in", 10, "done"),
  item("w-109", "portal", "eva", at(17), at(20, 17), "Regression run", 20, "to do"),
  item("w-110", "booking", "hana", at(9), at(13, 17), "Reminder scheduling service", 32, "done"),
  item("w-111", "booking", "hana", at(16), at(20, 17), "Push notifications", 30, "in progress"),
  item("w-112", "booking", "kofi", at(10), at(12, 17), "Calendar sync", 18, "done"),
  item("w-113", "intranet", "kofi", at(16), at(18, 17), "News feed", 16, "in progress"),
  item("w-114", "booking", "freya", at(9), at(11, 17), "Reminder settings screen", 16, "done"),
  item("w-115", "booking", "freya", at(18), at(20, 17), "Store screenshots", 12, "to do"),
  item("w-116", "booking", "david", at(16), at(19, 13), "Device test matrix", 14, "to do"),
];

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

const WEB = PEOPLE.filter((person) => person.team === "Web");
const WEB_WORK = WORK.filter((item) => WEB.some((person) => person.id === item.lane));

export default function DetailPanelInstead() {
  const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [day(9), day(21)] } });
  const [item, setItem] = useState<WorkItem | null>(null);

  const onInteraction = (interaction: ScheduleInteraction) => {
    if (interaction.type !== "hover") return;
    const hit = interaction.hit;
    setItem(hit.kind === "subtask" ? (WEB_WORK.find((one) => one.id === hit.subtask.id) ?? null) : null);
  };

  return (
    <Stack gap={3}>
      <Schedule
        ariaLabel="Sprint 14 of the web team"
        height={260}
        tooltip={false}
        onInteraction={onInteraction}
      >
        {WEB.map((person) => (
          <Lane key={person.id} id={person.id} label={person.name} />
        ))}
        <Subtasks data={WEB_WORK} tasks={PROJECTS} />
      </Schedule>
      <Card data-detail-panel>
        <CardBody>
          {item === null ? (
            <Text size="sm" tone="secondary">
              Rest the pointer on a work item to see its details.
            </Text>
          ) : (
            <Stack gap={1}>
              <Stack direction="row" gap={2} align="center">
                <Text size="sm" weight="semibold">
                  {item.name}
                </Text>
                <Badge tone={item.status === "done" ? "success" : "neutral"}>{item.status}</Badge>
              </Stack>
              <Text size="xs" tone="secondary">
                {PROJECTS.find((project) => project.id === item.task)?.name} · {PEOPLE.find((person) => person.id === item.lane)?.name} ·{" "}
                {item.estimate} h estimated
              </Text>
            </Stack>
          )}
        </CardBody>
      </Card>
    </Stack>
  );
}

Say what a bar covers and a line joins

The target says what is hovered: a bar brings its subtask and the blocked time it covers, a line its dependency with the subtasks it joins, from and to.

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

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

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

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

const HANDOVERS: Dependency[] = [
  { id: "to-build", from: "design", to: "build", lag: min(15) },
  { id: "to-test", from: "build", to: "test", lag: min(15) },
];

const LEAVE: BlockedTime[] = [{ id: "arjun-dentist", lane: "arjun", from: at(11), to: at(12), label: "Dentist" }];

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

function Covers({ target }: { target: ScheduleTooltipTarget }) {
  if (target.kind === "dependency") {
    return (
      <Stack gap={1}>
        <Text size="sm" weight="semibold">
          Handover {target.dependency.id}
        </Text>
        <Text size="xs" tone="secondary">
          {target.from?.name ?? "?"} → {target.to?.name ?? "?"}
        </Text>
      </Stack>
    );
  }
  if (target.kind === "milestone") return null;
  return (
    <Stack gap={1}>
      <Text size="sm" weight="semibold">
        {target.subtask.name}
      </Text>
      <Text size="xs" tone="secondary">
        {time(target.subtask.from)} – {time(target.subtask.to)}
      </Text>
      {target.blocked.map((blocked) => (
        <Text size="xs" tone="secondary" key={blocked.id}>
          Covers {blocked.label}, {time(blocked.from)} – {time(blocked.to)}
        </Text>
      ))}
    </Stack>
  );
}

export default function SayWhatABarCoversAndALineJoins() {
  const { Schedule, Lane, Subtasks, Dependencies, BlockedTimes } = useSchedule({ initialView: { domain: [at(6, 30), at(15, 30)] } });
  return (
    <Schedule ariaLabel="Design, build and test, with leave under the build" height={188} tooltip={(target) => <Covers target={target} />}>
      <Lane id="noah" label="Noah Fischer" />
      <Lane id="arjun" label="Arjun Mehta" />
      <Lane id="eva" label="Eva Novak" />
      <BlockedTimes data={LEAVE} />
      <Dependencies data={HANDOVERS} />
      <Subtasks data={WORK} tasks={PROJECTS} />
    </Schedule>
  );
}

Keyboard

The keys of First schedule apply here.

API

ScheduleTooltipTarget

NameTypeDefaultDescription
kindrequired| "subtask"
| "milestone"
| "dependency"
—A subtask is hovered. A milestone is hovered. A dependency is hovered.

Shown in: Write your own tooltip content, Say what a bar covers and a line joins, Say what a milestone is in your own tooltip (Milestones)

subtaskSubtask—The hovered subtask.

Shown in: Say what a bar covers and a line joins, Say what a milestone is in your own tooltip (Milestones)

taskTask | undefined—Its task, where the tasks name it.

Shown in: Write your own tooltip content

overlappingreadonly Subtask[]—The subtasks it overlaps with on its lane.

Shown in: Write your own tooltip content

violatedDependenciesreadonly ViolatedDependency[]—The violated dependencies leaving or reaching it.

Shown in: Write your own tooltip content

blockedreadonly BlockedTime[]—The blocked time it covers on its lane.

Shown in: Say what a bar covers and a line joins

milestoneMilestone—The hovered milestone.

Shown in: Say what a milestone is in your own tooltip (Milestones)

dependencyDependency—The hovered dependency.

Shown in: Say what a bar covers and a line joins

fromSubtask | undefined—The subtask it leaves.

Shown in: Say what a bar covers and a line joins

toSubtask | undefined—The subtask it reaches.

Shown in: Say what a bar covers and a line joins

violatedViolatedDependency | undefined—Its finding, where it is violated.

Shown in: Write your own tooltip content

Every page of @umriss-ui/schedule

Getting started

Plan

Time

Reading

Editing

Measuring

API index