The handle

Converts between a point on the screen and a time on a lane, so marks of your own - a cut-off, a delivery window, a pin - stand at the right place beside the plan.

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

The handle holds three functions and nothing else: clientPointOf(time, lane?), positionAt(clientX, clientY) and visibleDomain(). Everything else the schedule does is props.

Place your marks again whenever view.domain changes.

Examples

Place a mark of your own

clientPointOf turns a time into a point for your own mark; positionAt turns a pointer back into a time and a lane.

import { useEffect, useRef, useState } from "react";
import { Stack, Text } from "@umriss-ui/core";
import { useSchedule } from "@umriss-ui/schedule";
import type { ScheduleHandle, 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: "truck-118", label: "Truck FP 118 R" },
  { id: "van-402", label: "Van FP 402 R" },
  { id: "truck-520", label: "Truck FP 520 E" },
];

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-1", task: "c-2041", lane: "truck-118", from: at(6), to: at(7), leadIn: min(15), leadOut: min(10) },
  { 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(12), to: at(13, 30), leadIn: min(15) },
];

/** The last moment a parcel is taken for delivery today. */
const CUT_OFF = at(14);

const clock = (time: number) => new Date(time).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });

export default function PlaceAMark() {
  const { Schedule, Lane, Subtasks, view } = useSchedule({ initialView: { domain: [at(5, 30), at(18)] } });
  const plan = useRef<ScheduleHandle>(null);
  const host = useRef<HTMLDivElement>(null);
  const [pin, setPin] = useState<number | null>(null);
  const [under, setUnder] = useState("Move the pointer over the plan");

  /* The client point, turned into one inside this example - anew whenever the span moves. */
  useEffect(() => {
    const point = plan.current?.clientPointOf(CUT_OFF);
    const left = host.current?.getBoundingClientRect().left;
    setPin(point == null || left === undefined ? null : point.x - left);
  }, [view.domain]);

  return (
    <Stack gap={2}>
      <div ref={host} style={{ position: "relative", height: 12 }}>
        {pin !== null && (
          <Text size="xs" tone="muted" style={{ position: "absolute", top: -4, left: pin, transform: "translateX(-50%)", whiteSpace: "nowrap" }} data-pin>
            Same-day cut-off
          </Text>
        )}
      </div>
      <Schedule
        ref={plan}
        ariaLabel="Three vehicles with a cut-off of the application's own"
        height={196}
        onInteraction={(interaction) => {
          /* The interaction carries time and lane already; `positionAt` is for
             listeners of your own, which have only a point. */
          const there = plan.current?.positionAt(interaction.clientX, interaction.clientY);
          setUnder(there == null ? "off the plan" : `${clock(there.time)} on ${there.lane ?? "no lane"}`);
        }}
      >
        {VEHICLES.map((vehicle) => (
          <Lane key={vehicle.id} id={vehicle.id} label={vehicle.label} />
        ))}
        <Subtasks data={LEGS} tasks={CONSIGNMENTS} />
      </Schedule>
      <Text size="sm" mono tone="secondary" data-under-pointer>
        {under}
      </Text>
    </Stack>
  );
}

When to use something else

Keyboard

The keys of First schedule apply here.

Props on this page

From ScheduleProps — the full table stands on First schedule.

NameTypeDefaultDescription
refRef<ScheduleHandle>—Receives the ScheduleHandle - not an element.
heightnumber400Height in CSS pixels, both bands included. The width is the host's.
childrenReactNode—Lane, Dependencies and Subtasks, in the order they are to stand and be drawn.

Events

NameTypeDefaultDescription
onInteraction(interaction: ScheduleInteraction) => void—Click, context menu and hover, with what the pointer is on and where. While it is set, a right-click opens no browser menu.

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.

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 ScheduleInteraction — the full table stands on Interactions.

NameTypeDefaultDescription
clientXrequirednumber—The pointer's horizontal client coordinate - where a ContextMenu opens.
clientYrequirednumber—The pointer's vertical client coordinate.

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