What a bar says besides its colour: provisional, fixed, muted, running on past the view, and how far the work has got. Each takes one channel of the drawing, so several on one bar stay readable.
import { useSchedule, resolveAppearance } from "@umriss-ui/schedule";
"provisional" owns the fill (none at all), "fixed" the ends (a cap inside each), "muted" the saturation, "open" a fade at the edge of the view, and progress a rail inside the main time. The faint, outlined fill belongs to a lead-in and a lead-out and to nothing else.
"provisional" and "fixed" contradict, and the later one in the list wins. resolveAppearance is that rule, exported for the application to read as well.
Work pencilled in but not agreed: "provisional" leaves the bar hollow with a dashed outline, never pale like a lead-in.
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 PROJECTS: Task[] = [{ id: "portal", name: "Member portal", color: "light-dark(#2563eb, #6b9bff)" }];
const WORK: Subtask[] = [
{ id: "agreed", task: "portal", lane: "released", from: at(7), to: at(10), leadIn: min(30), leadOut: min(30) },
{ id: "pencilled", task: "portal", lane: "draft", from: at(7), to: at(10), appearance: ["provisional"] },
];
export default function Provisional() {
const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(6, 30), at(11)] } });
return (
<Schedule ariaLabel="Agreed work above, provisional work below" height={144}>
<Lane id="released" label="Agreed, with lead-in" />
<Lane id="draft" label="Provisional" />
<Subtasks data={WORK} tasks={PROJECTS} />
</Schedule>
);
}
A delivery slot the customer booked may not move: "fixed" sets a cap inside each end and keeps the face clear for the label.
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 TOURS: Task[] = [{ id: "t-05", name: "T-05 Riverside", color: "light-dark(#2563eb, #6b9bff)" }];
const LEGS: Subtask[] = [
{ id: "movable", task: "t-05", lane: "movable", from: at(7), to: at(10) },
{ id: "nailed", task: "t-05", lane: "nailed", from: at(7), to: at(10), appearance: ["fixed"] },
];
export default function Fixed() {
const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(6, 30), at(11)] } });
return (
<Schedule
ariaLabel="A round that may move above, a booked slot below"
height={144}
label={(leg) => (leg.id === "nailed" ? "T-05 · booked slot" : "T-05")}
>
<Lane id="movable" label="May move" />
<Lane id="nailed" label="Fixed" />
<Subtasks data={LEGS} tasks={TOURS} />
</Schedule>
);
}
Another team's work, shown for context: "muted" mixes the colour into the surface at full height, so it cannot pass for a lead-in.
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 TASKS: Task[] = [{ id: "migration", name: "Checkout migration", color: "light-dark(#2563eb, #6b9bff)" }];
const WORK: Subtask[] = [
{ id: "ours", task: "migration", lane: "ours", from: at(7), to: at(10), leadIn: min(30) },
{ id: "theirs", task: "migration", lane: "theirs", from: at(7), to: at(10), leadIn: min(30), appearance: ["muted"] },
];
export default function Muted() {
const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(6, 30), at(11)] } });
return (
<Schedule
ariaLabel="Our team's work above, another team's below"
height={144}
label={() => "Checkout migration"}
>
<Lane id="ours" label="Payments" />
<Lane id="theirs" label="Identity" />
<Subtasks data={WORK} tasks={TASKS} />
</Schedule>
);
}
progress from 0 to 1 draws a rail inside the main time; left out, the bar claims nothing - which is not zero per cent.
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 PROJECTS: Task[] = [{ id: "portal", name: "Member portal", color: "light-dark(#2563eb, #6b9bff)" }];
const WORK: Subtask[] = [
{ id: "unclaimed", task: "portal", lane: "unclaimed", from: at(7), to: at(10) },
{ id: "started", task: "portal", lane: "started", from: at(7), to: at(10), progress: 0.65, leadOut: min(30) },
];
export default function Progress() {
const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(6, 30), at(11)] } });
return (
<Schedule ariaLabel="Work claiming nothing above, work at 65 per cent below" height={144}>
<Lane id="unclaimed" label="Says nothing" />
<Lane id="started" label="65 per cent done" />
<Subtasks data={WORK} tasks={PROJECTS} />
</Schedule>
);
}
An incident still being worked runs past the view: "open" fades the bar at whichever edge it passes, and only there.
import { useSchedule } from "@umriss-ui/schedule";
import type { Subtask, Task } from "@umriss-ui/schedule";
/* The middle bar is wholly in view and carries no fade; zoom out until it
passes an edge and the fade appears. */
const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
const TASKS: Task[] = [{ id: "inc-1048", name: "INC-1048", color: "light-dark(#2563eb, #6b9bff)" }];
const WORK: Subtask[] = [
{ id: "runs-on", task: "inc-1048", lane: "runs-on", from: at(7), to: at(13), appearance: ["open"] },
{ id: "whole", task: "inc-1048", lane: "whole", from: at(7), to: at(10), appearance: ["open"] },
{ id: "began", task: "inc-1048", lane: "began", from: at(4), to: at(10), appearance: ["open"] },
];
export default function Open() {
const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(6, 30), at(11)] } });
return (
<Schedule ariaLabel="Work running past the view at either edge" height={188}>
<Lane id="runs-on" label="Runs on" />
<Lane id="whole" label="Wholly in view" />
<Lane id="began" label="Began before" />
<Subtasks data={WORK} tasks={TASKS} />
</Schedule>
);
}
Muted, fixed and open on one bar stay three statements, each in its own channel; of "provisional" and "fixed" the later one wins.
import { useSchedule } from "@umriss-ui/schedule";
import type { Subtask, Task } from "@umriss-ui/schedule";
/* A hollow bar lies on the surface, so its label takes the page's text
colour; a muted one takes whatever reads on its mix. */
const at = (hours: number, minutes = 0) => new Date(2026, 2, 17, hours, minutes).getTime();
const TOURS: Task[] = [
{ id: "own", name: "North depot", color: "light-dark(#2563eb, #6b9bff)" },
{ id: "other", name: "Riverside depot", color: "light-dark(#c2410c, #f08a52)" },
];
const LEGS: Subtask[] = [
/* Paler colour, capped ends, faded edge. */
{
id: "all-three",
task: "other",
lane: "all-three",
from: at(7),
to: at(13),
appearance: ["fixed", "muted", "open"],
leadIn: 30 * 60_000,
},
/* The rail lies on the surface the hollow bar leaves. */
{ id: "planned", task: "own", lane: "planned", from: at(7), to: at(10), appearance: ["provisional"], progress: 0.4 },
/* `"fixed"` is the later word, so this bar is fixed. */
{ id: "settled", task: "own", lane: "settled", from: at(7), to: at(10), appearance: ["provisional", "fixed"] },
/* And the other way round, which makes it provisional. */
{ id: "reopened", task: "own", lane: "reopened", from: at(7), to: at(10), appearance: ["fixed", "provisional"] },
];
/** What stands in each bar. */
const NAMES: Record<string, string> = {
"all-three": "T-06 · Riverside",
planned: "T-02 · draft",
settled: "T-02 · released",
reopened: "T-02 · reopened",
};
const LANES = [
{ id: "all-three", label: "Other depot, fixed, runs on" },
{ id: "planned", label: "Provisional, 40 per cent" },
{ id: "settled", label: "provisional then fixed" },
{ id: "reopened", label: "fixed then provisional" },
];
export default function Combinations() {
const { Schedule, Lane, Subtasks } = useSchedule({ initialView: { domain: [at(6, 30), at(11)] } });
return (
<Schedule
ariaLabel="Bars carrying several appearances at once"
height={230}
label={(leg) => NAMES[leg.id] ?? leg.id}
>
{LANES.map((lane) => (
<Lane key={lane.id} id={lane.id} label={lane.label} />
))}
<Subtasks data={LEGS} tasks={TOURS} />
</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. |
label | (subtask: Subtask) => string | — | What stands written in a bar: a function from a subtask to a line of text, or nothing for bars without text. The text is cut off with an ellipsis where the bar is too narrow for it and left out where even that would say nothing; a bar that began before the view keeps its text at the view's edge. |
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. |
appearance | readonly SubtaskAppearance[] | — | What the bar says besides its colour: "provisional", "fixed",
"muted", "open". Several hold at once; "provisional" and "fixed"
contradict, and the later one in the list wins. |
progress | number | — | The share of the work that is done, from 0 to 1, drawn as a filled part of the bar. Without it nothing is claimed - an empty bar and a bar at zero per cent are two different statements. |
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. |