Day, week, work week, month and list views over one event model. Overlapping events share columns, multi-day events pack into lanes, and you can create, move and resize by dragging (snapped) or from the keyboard. A now line, business hours, holidays and an explicit time zone are built in. Our own engine on date-fns, with no calendar library underneath.
November 8 – 14, 2026
Installation
The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";
Usage
import { Scheduler } from "@vamos-ui/react/scheduler";
<Scheduler
events={events} // { id, title, start, end } — UTC ISO, or YYYY-MM-DD with allDay
defaultView="week"
timeZone="Europe/Istanbul"
onCreate={(range) => openCreateDialog(range)}
onEventChange={(c) => api.events.update(c)} // optimistic; reject to roll back
onEventOpen={(id) => router.push(`/events/${id}`)}
/>
// The React-free layout functions are exported from the same entry
import { layoutDayEvents, packLanes } from "@vamos-ui/react/scheduler";Keyboard: events are focusable in time order. Alt+↑/↓ moves by the snap step, Alt+Shift+↑/↓ resizes, and Alt+←/→ changes the day. The context menu (right-click, Shift+F10) has “Change time…”, a dialog with date and time fields. This is the non-drag path for WCAG 2.5.7.
Events and bars are opaque colour mixes over the solid surface. They never blur, so the glass material stays fast. The drag preview uses --popover.
Week start, weekend and 12/24-hour clock come from the locale. Titles use Intl formatRange (“November 8 – 14, 2026”, “9–15 Kasım 2026”).
Recurrence (RRULE) and ICS export are planned for a later release.
Examples
Month with multi-day lanes
Multi-day events span the week and pack into lanes; a crowded day shows “+N more” with a popover. The month is an ARIA grid: arrow keys move between days, Enter opens the day.
November 2026
Create, move, resize and roll back
HR interview scheduling. Changes appear at once; this fake API rejects slots after 17:00, so the event snaps back and a toast explains why. Locked events (editable: false) don't move.
Drag on empty time to create, drag an event to move it, pull its bottom edge to resize. Keyboard: Alt + ↑/↓ moves by 15 min, Alt + Shift + ↑/↓ resizes, Alt + ←/→ changes the day. Right-click for “Change time…”.
November 8 – 14, 2026
Business hours and holidays
A spa booking back office on trWorkCalendar: 09:00–18:00, Republic Day off, its eve shaded from 13:00. restrictToBusinessHours outlines an invalid drop in red and refuses it.
26–30 Ekim 2026
List (agenda) view
The screen-reader-first view: one heading per day and one button per event, with time, title, place and status in reading order. The empty state reads “Nothing scheduled this week”.
November 8 – 14, 2026
9MondayNovember 2026
10TuesdayNovember 2026
11WednesdayNovember 2026
13FridayNovember 2026
Editorial calendar
A CMS publishing calendar with a channel filter in the toolbar slot and a custom renderEvent.
November 8 – 14, 2026
Same events, two time zones
Events are stored in UTC. Each scheduler shows them in its own zone, with the offset in the corner of the time axis. Day boundaries, the now line and every instant it gives you back use that zone.
Europe/Istanbul
Tuesday, November 10, 2026
America/New_York
Tuesday, November 10, 2026
API
Scheduler
| Prop | Type | Default |
|---|---|---|
events{ id, title, start, end, allDay?, tone?, status?, editable?, location?, data? }. Timed events use UTC ISO instants (end exclusive). All-day events use YYYY-MM-DD (end inclusive). | SchedulerEvent[] | — |
view / defaultView / onViewChangeThe current view. | "day" | "week" | "workweek" | "month" | "list" | "week" |
viewsViews offered in the switch. Pass one view to hide the switch. | SchedulerView[] | ["day", "week", "month", "list"] |
date / defaultDate / onDateChangeThe focused day. Navigation moves by the view's length. | string (YYYY-MM-DD) | today |
onRangeChangeFires when the visible days change (inclusive YYYY-MM-DD). Use it to fetch events. | ({ start, end }) => void | — |
timeZoneEvery time shown and every instant returned uses this zone. The offset is shown on the time axis. | string (IANA) | LocaleProvider timeZone |
slotMinutesGrid step, and the length of keyboard-created events. | 5 | 10 | 15 | 30 | 60 | 30 |
snapMinutesDrag and Alt+arrow step. Hold Alt while dragging for 5-minute steps. | number | 15 |
dayRangeThe part of the day the time grid shows. | { start: "07:00", end: "21:00" } | 00:00–24:00 |
scrollToTimeTime scrolled into view on open. | string (HH:mm) | business-hours start or 08:00 |
heightMax height of the scrolling time grid. | string | number | "36rem" |
workCalendar{ workDays, businessHours, holidays, region }. Shades non-working time, holidays and half days. | WorkCalendar | from locale (tr → Turkish holidays) |
restrictToBusinessHoursRefuses moves and new events outside business hours (red outline while dragging). | boolean | false |
onCreateDrag on empty time, double-click, or the keyboard slot cursor (arrow keys, then Enter). | ({ start, end, allDay }) => void | — |
onEventChangeMove or resize. Applied at once; a rejected promise reverts it and shows a toast. | ({ id, start, end, allDay }) => void | Promise<void> | — |
onEventOpenClick or Enter on an event. | (id, event) => void | — |
renderEventCustom event content. The accessible name stays the built-in description. | (event, { view, compact, timeLabel }) => ReactNode | — |
maxEventsPerDayMonth view: bars per day before “+N more”. | number | 3 |
variantembedded drops the toolbar and frame, for cards and side panels. | "full" | "embedded" | "full" |
toolbarExtra toolbar content, such as filters or a “New event” button. | ReactNode | — |
loadingDims events (old ones stay visible) and shows a spinner. | boolean | false |
readOnlyNo create, move or resize. | boolean | false |
listDaysList view length, counted from the week start. | number | 7 |
nowA fixed “now” for tests and screenshots. | Date | number | — |
labelsOverrides for the scheduler namespace strings. | Partial<SchedulerLabels> | — |
core (React-free)
| Prop | Type | Default |
|---|---|---|
layoutDayEvents(events, day, timeZone)Packs overlapping timed events into columns: startMin, endMin, column, columns, span, continuesBefore/After. | PositionedEvent[] | — |
packLanes(events, range, timeZone, { include })Packs all-day and multi-day bars into lanes over a day range. include: "all" also packs timed events (month view). | LaneAssignment[] | — |
shiftEvent / instantAt / localDate / localMinutesWall-clock maths in an explicit zone, safe across DST. | functions | — |
weekDays / monthWeeksWeek and month grids from the locale's first day. | (date, firstDay) => string[] / string[][] | — |