# Tooltip

> Package `@umriss-ui/schedule`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/schedule/tooltip/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/schedule/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/schedule/llms-full.txt).

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.

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

Demo page: https://romanhaendler.github.io/umriss-ui/schedule/tooltip/

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.

```tsx
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.

```tsx
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`.

```tsx
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`.

```tsx
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](https://romanhaendler.github.io/umriss-ui/schedule/schedule/#keyboard-schedule) apply here.

## API

### `ScheduleTooltipTarget`

| Prop | Type | Default | Description |
|---|---|---|---|
| `kind` *required* | `"subtask" \| "milestone" \| "dependency"` | — | A subtask is hovered. A milestone is hovered. A dependency is hovered.<br>Shown in: [Write your own tooltip content](https://romanhaendler.github.io/umriss-ui/schedule/tooltip/#own-tooltip), [Say what a bar covers and a line joins](https://romanhaendler.github.io/umriss-ui/schedule/tooltip/#say-what-a-bar-covers-and-a-line-joins), [Say what a milestone is in your own tooltip (Milestones)](https://romanhaendler.github.io/umriss-ui/schedule/milestones/#say-what-a-milestone-is) |
| `subtask` | [`Subtask`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#type-Subtask) | — | The hovered subtask.<br>Shown in: [Say what a bar covers and a line joins](https://romanhaendler.github.io/umriss-ui/schedule/tooltip/#say-what-a-bar-covers-and-a-line-joins), [Say what a milestone is in your own tooltip (Milestones)](https://romanhaendler.github.io/umriss-ui/schedule/milestones/#say-what-a-milestone-is) |
| `task` | [`Task`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#type-Task)` \| undefined` | — | Its task, where the tasks name it.<br>Shown in: [Write your own tooltip content](https://romanhaendler.github.io/umriss-ui/schedule/tooltip/#own-tooltip) |
| `overlapping` | `readonly `[`Subtask`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#type-Subtask)`[]` | — | The subtasks it overlaps with on its lane.<br>Shown in: [Write your own tooltip content](https://romanhaendler.github.io/umriss-ui/schedule/tooltip/#own-tooltip) |
| `violatedDependencies` | `readonly `[`ViolatedDependency`](https://romanhaendler.github.io/umriss-ui/schedule/findings/#type-ViolatedDependency)`[]` | — | The violated dependencies leaving or reaching it.<br>Shown in: [Write your own tooltip content](https://romanhaendler.github.io/umriss-ui/schedule/tooltip/#own-tooltip) |
| `blocked` | `readonly `[`BlockedTime`](https://romanhaendler.github.io/umriss-ui/schedule/blocked-time/#type-BlockedTime)`[]` | — | The blocked time it covers on its lane.<br>Shown in: [Say what a bar covers and a line joins](https://romanhaendler.github.io/umriss-ui/schedule/tooltip/#say-what-a-bar-covers-and-a-line-joins) |
| `milestone` | [`Milestone`](https://romanhaendler.github.io/umriss-ui/schedule/milestones/#type-Milestone) | — | The hovered milestone.<br>Shown in: [Say what a milestone is in your own tooltip (Milestones)](https://romanhaendler.github.io/umriss-ui/schedule/milestones/#say-what-a-milestone-is) |
| `dependency` | [`Dependency`](https://romanhaendler.github.io/umriss-ui/schedule/dependencies/#type-Dependency) | — | The hovered dependency.<br>Shown in: [Say what a bar covers and a line joins](https://romanhaendler.github.io/umriss-ui/schedule/tooltip/#say-what-a-bar-covers-and-a-line-joins) |
| `from` | [`Subtask`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#type-Subtask)` \| undefined` | — | The subtask it leaves.<br>Shown in: [Say what a bar covers and a line joins](https://romanhaendler.github.io/umriss-ui/schedule/tooltip/#say-what-a-bar-covers-and-a-line-joins) |
| `to` | [`Subtask`](https://romanhaendler.github.io/umriss-ui/schedule/subtasks/#type-Subtask)` \| undefined` | — | The subtask it reaches.<br>Shown in: [Say what a bar covers and a line joins](https://romanhaendler.github.io/umriss-ui/schedule/tooltip/#say-what-a-bar-covers-and-a-line-joins) |
| `violated` | [`ViolatedDependency`](https://romanhaendler.github.io/umriss-ui/schedule/findings/#type-ViolatedDependency)` \| undefined` | — | Its finding, where it is violated.<br>Shown in: [Write your own tooltip content](https://romanhaendler.github.io/umriss-ui/schedule/tooltip/#own-tooltip) |
