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.tsxAbout
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
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.
<Scheduler
events={events}
week={monday}
days={5}
dayStart={8}
dayEnd={18}
/>Examples
A working week, with a clash
<Scheduler events={events} onChange={setEvents} onCreate={make} week={monday} days={5} />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'>.
| Prop | Type | Default | Description |
|---|---|---|---|
| events | SchedulerEvent[] | 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 | — | — |
| week | Date | — | Any date inside the week to show. |
| weekStartsOn | number | 1 | 0 is Sunday, 1 Monday. |
| days | number | 7 | — |
| dayStart | number | 8 | Crops to working hours; eight of the twenty-four rows carry all the information in most calendars. |
| dayEnd | number | 20 | Crops to working hours; eight of the twenty-four rows carry all the information in most calendars. |
| hourHeight | number | 64 | Pixels 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. |
| snapMinutes | number | 15 | The unit calendars are actually kept in. Without a grid a drag produces 10:07–11:04. |
| locale | string | — | — |
| emptyLabel | string | 'Nothing this week.' | — |
| label | string | 'Schedule' | — |
Types
SchedulerEvent
A week of days with events laid out against the hours, and edited in place.
| Field | Type | Description |
|---|---|---|
| id | string | — |
| title | ReactNode | — |
| start | Date | string | — |
| end | Date | string | — |
| allDay? | boolean | Drawn as a full-width band above the grid. |
| locked? | boolean | Excluded from dragging and keyboard editing. |
Behaviour
| Topic | Detail | Description |
|---|---|---|
| vs Calendar | different jobs | Calendar picks a date; this shows what is in it. Conflating them produces a date picker with a broken agenda glued on. |
| overlaps | columns, not stacks | Two 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. |
| timezone | local day keys | Keying 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. |
| dragging | move, resize, create | The 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. |
| keyboard | arrows and shift-arrow | Up and down move by a snap, left and right move between days, shift-arrow changes the end. |
| monochrome | a hairline, not a hue | Adjacent 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. |