Scheduler

A week of days with events laid out against the hours and edited in place — drag to move across days, drag an edge to change the end, drag empty grid to create. Overlaps split into columns rather than stacking.

src/components/ui/scheduler.tsx

About

From the component's own source, where it was written.

A week of days with events laid out against the hours, and edited in place.

**Calendar picks a date; this shows what is in it.** They are different components with different jobs, and conflating them produces a date picker with a broken agenda glued on.

**A calendar you cannot drag on is a printout.** Moving a meeting, making it longer, and blocking out an hour are the three things people do with a week view, so all three are direct: drag an event to move it — across days as well as hours — drag its bottom edge to change the end, and drag on empty grid to create.

**Everything snaps to snapMinutes.** Fifteen is the unit calendars are actually kept in; without a grid a drag produces 10:07–11:04 and nobody wants that meeting.

**Events are cards, in the same surface the rest of the kit uses.** A week view is a reading surface, not a heat map: a card with a border, a title and a time is scannable, and it sits in a page beside other cards without looking like a different product. A solid rail down the leading edge is what marks it as an event, and it is the one element that stays at full strength when the card is dimmed.

**It is monochrome.** A week is read for shape — where the gaps are, what collides — and colouring events by an arbitrary category adds a legend without adding information. Overlapping blocks are separated by a hairline in the page's own background rather than by hue, which keeps them distinct at any theme and in print.

**Overlapping events are laid out in columns, not stacked.** Two meetings at the same time drawn on top of each other means one is invisible, which is the worst thing a calendar can do — the hidden one is the one you miss. Overlaps are grouped into clusters and each cluster is split into as many columns as it needs, so every event keeps a visible edge, and that stays true while you are dragging one on top of another.

**Times are rendered in the caller's timezone and days are keyed by local date.** A calendar that keys by UTC puts a 23:00 event on the wrong day for anyone west of Greenwich — a common bug, and invisible from the timezone it was written in.

Events are also editable from the keyboard: arrow keys move by a snap, shift-arrow changes the end, and the current time is announced through aria-valuetext.

Install

npx astralyx-ui add scheduler

Usage

import { Scheduler } from '@/components/ui/scheduler'

<Scheduler events={events} onChange={setEvents} onCreate={make} dayStart={8} dayEnd={20} />

Composer

Change the props and the source below follows.

Schedule

Mon6
Tue7
Wed8
Thu9
Fri10
08:00
09:00
10:00
11:00
12:00
13:00
14:00
15:00
16:00
17:00
Standup09:30 AM
Design review11:00 AM – 12:00 PM
On-call handover11:30 AM – 12:30 PM
Standup09:30 AM
Pairing — encoder01:00 PM – 03:00 PM
Standup09:30 AM
Retro04:00 PM – 05:00 PM
Interview10:00 AM – 11:00 AM
Planning02:00 PM – 04:00 PM
Demo03:00 PM – 04:00 PM

Drag an event to move it — across days too — its bottom edge to change the end, or drag on empty grid to block out time.

Props
tsx
<Scheduler
  events={events}
  week={monday}
  days={5}
  dayStart={8}
  dayEnd={18}
/>

Examples

A working week, with a clash

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

Scheduler

Extends Omit<ComponentProps<'div'>, 'onSelect' | 'onChange'>.

PropTypeDefaultDescription
eventsSchedulerEvent[]required{ id, title, start, end, color?, allDay? }.
onChange(events: SchedulerEvent[]) => void—Supplying either makes the grid editable. A calendar you cannot drag on is a printout.
onEventChange(event: SchedulerEvent, previous: SchedulerEvent) => void—Supplying either makes the grid editable. A calendar you cannot drag on is a printout.
onCreate(start: Date, end: Date) => SchedulerEvent | void—Return an event to add it, or nothing to decline — so a click-drag can open your own dialog instead.
onSelect(event: SchedulerEvent) => void——
weekDate—Any date inside the week to show.
weekStartsOnnumber10 is Sunday, 1 Monday.
daysnumber7—
dayStartnumber8Crops to working hours; eight of the twenty-four rows carry all the information in most calendars.
dayEndnumber20Crops to working hours; eight of the twenty-four rows carry all the information in most calendars.
hourHeightnumber64Pixels per hour. Generous by default — a week view is a reading surface, and an hour compressed to 40px cannot hold a title and a time.
snapMinutesnumber15The unit calendars are actually kept in. Without a grid a drag produces 10:07–11:04.
localestring——
emptyLabelstring'Nothing this week.'—
labelstring'Schedule'—

Types

SchedulerEvent

A week of days with events laid out against the hours, and edited in place.

FieldTypeDescription
idstring—
titleReactNode—
startDate | string—
endDate | string—
allDay?booleanDrawn as a full-width band above the grid.
locked?booleanExcluded from dragging and keyboard editing.

Behaviour

TopicDetailDescription
vs Calendardifferent jobsCalendar picks a date; this shows what is in it. Conflating them produces a date picker with a broken agenda glued on.
overlapscolumns, not stacksTwo meetings drawn on top of each other means one is invisible — and the hidden one is the one you miss. Clusters are split into as many columns as they need.
timezonelocal day keysKeying by UTC date puts a 23:00 event on the wrong day for anyone west of Greenwich — common, and hard to spot from the timezone it was written in.
draggingmove, resize, createThe body moves the event across hours *and* days; the bottom edge changes the end; a drag on empty grid draws a new one. The grab offset is kept, so the event does not jump its top to the cursor.
keyboardarrows and shift-arrowUp and down move by a snap, left and right move between days, shift-arrow changes the end.
monochromea hairline, not a hueAdjacent columns are separated by a border in the page background rather than by colour, so a clash stays readable at any theme and in print.