Two subtasks claiming one lane at the same time (also called a double booking or conflict): both drawn, offset and marked, never packed into sub-lanes.
import { useSchedule } from "@umriss-ui/schedule";
The later bar is offset a few pixels and edged, and the shared time is marked across the lane. Lead-in and lead-out occupy the lane too, so a lead-out meeting the next lead-in is an overlap. The offset stops at three levels and never leaks into the lane below.
Both bars stay where their times put them: the later one is offset and edged, and the shared time is marked across the lane.
import { useSchedule } from "@umriss-ui/schedule";
import type { Subtask, Task } from "@umriss-ui/schedule";
const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
const PROJECTS: Task[] = [
{ id: "booking", name: "Booking app", color: "light-dark(#0d9488, #3cc7b8)" },
{ id: "intranet", name: "Intranet", color: "light-dark(#be185d, #f06aa6)" },
];
const WORK: Subtask[] = [
{ id: "calendar-sync", task: "booking", lane: "kofi", from: at(9), to: at(12, 30) },
{ id: "news-feed", task: "intranet", lane: "kofi", from: at(11), to: at(15) },
{ id: "reminders", task: "booking", lane: "hana", from: at(9), to: at(13) },
];
export default function DoubleBooking() {
const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(8), at(16)] } });
return (
<Schedule ariaLabel="Kofi booked on two projects at once" height={150}>
<Lane id="kofi" label="Kofi Mensah" />
<Lane id="hana" label="Hana Sato" />
<Subtasks data={WORK} tasks={PROJECTS} />
</Schedule>
);
}
Unloading and the next loading occupy the vehicle too: where a leadOut runs into the next leadIn, that is an overlap.
import { useSchedule } from "@umriss-ui/schedule";
import type { 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 TOURS: Task[] = [
{ id: "t-04", name: "T-04 Ringway", color: "light-dark(#0d9488, #3cc7b8)" },
{ id: "t-06", name: "T-06 Riverside", color: "light-dark(#be185d, #f06aa6)" },
];
const LEGS: Subtask[] = [
{ id: "t-04-1", task: "t-04", lane: "fp-402", from: at(7), to: at(8, 30), leadOut: min(30) },
{ id: "t-06-1", task: "t-06", lane: "fp-402", from: at(9, 15), to: at(10, 45), leadIn: min(30) },
];
export default function LeadOutMeetsLeadIn() {
const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(6, 30), at(11, 30)] } });
return (
<Schedule ariaLabel="Two rounds of one van, touching at unloading and loading" height={110}>
<Lane id="fp-402" label="Van FP 402 R" />
<Subtasks data={LEGS} tasks={TOURS} />
</Schedule>
);
}
Each further bar steps down a little, three levels deep at most, and every one stays inside its own lane.
import { useSchedule } from "@umriss-ui/schedule";
import type { Subtask, Task } from "@umriss-ui/schedule";
const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
const INCIDENTS: Task[] = [
{ id: "inc-1046", name: "INC-1046", color: "light-dark(#2563eb, #6b9bff)" },
{ id: "inc-1047", name: "INC-1047", color: "light-dark(#c2410c, #f08a52)" },
{ id: "inc-1048", name: "INC-1048", color: "light-dark(#7c3aed, #a98bfa)" },
];
const WORK: Subtask[] = [
{ id: "a", task: "inc-1046", lane: "priya", from: at(8), to: at(11) },
{ id: "b", task: "inc-1047", lane: "priya", from: at(9), to: at(12) },
{ id: "c", task: "inc-1048", lane: "priya", from: at(10), to: at(13) },
{ id: "d", task: "inc-1048", lane: "jonas", from: at(10), to: at(12) },
];
export default function ThreeAtOnce() {
const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(7), at(14)] } });
return (
<Schedule ariaLabel="Priya on three incidents at once" height={150}>
<Lane id="priya" label="Priya Raman" />
<Lane id="jonas" label="Jonas Keller" />
<Subtasks data={WORK} tasks={INCIDENTS} />
</Schedule>
);
}
The keys of First schedule apply here.
From ScheduleProps — the full table stands on First schedule.
| Name | Type | Default | Description |
|---|---|---|---|
height | number | 400 | Height in CSS pixels, both bands included. The width is the host's. |
children | ReactNode | — | Lane, Dependencies and Subtasks, in the order they are to stand and
be drawn. |
| Name | Type | Default | Description |
|---|---|---|---|
ariaLabelrequired | string | — | What the schedule shows, for a screen reader: "Plan of week 38". |
From LaneProps — the full table stands on Lanes.
From SubtasksProps — the full table stands on Subtasks.
From Subtask — the full table stands on Subtasks.
| Name | Type | Default | Description |
|---|---|---|---|
idrequired | string | — | The caller's identity of the subtask. |
taskrequired | string | — | The id of the task it belongs to. |
lanerequired | string | — | The id of the lane it sits on. |
fromrequired | number | — | Start of the main time. |
torequired | number | — | End of the main time. |
leadIn | number | — | Duration of the lead-in before the main time; none is zero. |
leadOut | number | — | Duration of the lead-out after the main time; none is zero. |
From Task — the full table stands on Subtasks.
From ScheduleOptions — the full table stands on View.
| Name | Type | Default | Description |
|---|---|---|---|
initialView | ScheduleView | — | The view to start from, read once when the hook first runs; a different
value on a later render is ignored, as React ignores a changed
defaultValue. To go to a view later, call setView. The schedule
remembers none: where a view is kept is the application's decision. |
What umriss deliberately does not build, and why: ADR-0032.