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.
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>
);
}
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>
);
}
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>
);
}
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>
);
}
The keys of First schedule apply here.
ScheduleTooltipTarget