Appearances

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.

Examples

Provisional

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>
  );
}

Fixed

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>
  );
}

Muted

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

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>
  );
}

Open

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>
  );
}

Several at once

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>
  );
}

Keyboard

The keys of First schedule apply here.

Props on this page

From ScheduleProps — the full table stands on First schedule.

NameTypeDefaultDescription
heightnumber400Height 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.
childrenReactNode—Lane, Dependencies and Subtasks, in the order they are to stand and be drawn.

Accessibility

NameTypeDefaultDescription
ariaLabelrequiredstring—What the schedule shows, for a screen reader: "Plan of week 38".

From LaneProps — the full table stands on Lanes.

NameTypeDefaultDescription
idrequiredstring—The identity subtasks name in their lane.
labelReactNodeidWhat the lane header says - the person, vehicle or room. Real text, read by a screen reader.

From SubtasksProps — the full table stands on Subtasks.

NameTypeDefaultDescription
datarequiredreadonly Subtask[]—The subtasks, as the caller holds them. The schedule never changes one.
tasksrequiredreadonly Task[]—The tasks the subtasks belong to - their identity and their colour. A subtask whose task is not among them is drawn in a muted colour.

From Subtask — the full table stands on Subtasks.

NameTypeDefaultDescription
idrequiredstring—The caller's identity of the subtask.
taskrequiredstring—The id of the task it belongs to.
lanerequiredstring—The id of the lane it sits on.
fromrequirednumber—Start of the main time.
torequirednumber—End of the main time.
leadInnumber—Duration of the lead-in before the main time; none is zero.
leadOutnumber—Duration of the lead-out after the main time; none is zero.
appearancereadonly 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.
progressnumber—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.

NameTypeDefaultDescription
idrequiredstring—The caller's identity of the task.
colorrequiredstring—Any CSS colour - a literal, a token, light-dark(…). The colour means what the caller's domain needs it to mean.
namestring—What the task is called, where the schedule names it.

From ScheduleOptions — the full table stands on View.

NameTypeDefaultDescription
initialViewScheduleView—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.

From ScheduleView — the full table stands on View.

NameTypeDefaultDescription
domainreadonly [number, number]—The span in view, as two wall-clock instants. Absent, the schedule shows the extent of its subtasks, within zoomLimits.

Every page of @umriss-ui/schedule

Getting started

Plan

Time

Reading

Editing

Measuring

API index