ComponentsCalendar & time

Agenda / scheduler

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.

Own engine: date-fns v4 + @date-fns/tz, usePointerDrag scheduler/scheduler.tsx, scheduler/core/layout.ts, scheduler/shared.ts, sortable/use-pointer-drag.ts, styles/calendar.csstheme · material · accent · density · font

November 8 – 14, 2026

Times in Europe/Istanbul
Sun8
Mon9
Tue10
Wed11
Thu12
Fri13
Sat14
All day

Installation

$ pnpm add @vamos-ui/react

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

Sun
Mon
Tue
Wed
Thu
Fri
Sat
Sunday, November 1, 0 events
Monday, November 2, 1 event
Tuesday, November 3, 1 event
Wednesday, November 4, 1 event
Thursday, November 5, 0 events
Friday, November 6, 0 events
Saturday, November 7, 0 events
Sunday, November 8, 0 events
Monday, November 9, 1 event
Tuesday, November 10, 1 event
Wednesday, November 11, 2 events
Thursday, November 12, 6 events
Friday, November 13, 1 event
Saturday, November 14, 1 event
Sunday, November 15, 1 event
Monday, November 16, 1 event
Tuesday, November 17, 1 event
Wednesday, November 18, 1 event
Thursday, November 19, 1 event
Friday, November 20, 0 events
Saturday, November 21, 0 events
Sunday, November 22, 0 events
Monday, November 23, 0 events
Tuesday, November 24, 1 event
Wednesday, November 25, 0 events
Thursday, November 26, 0 events
Friday, November 27, 1 event
Saturday, November 28, 1 event
Sunday, November 29, 1 event
Monday, November 30, 1 event
Tuesday, December 1, 0 events
Wednesday, December 2, 0 events
Thursday, December 3, 0 events
Friday, December 4, 0 events
Saturday, December 5, 0 events

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

Times in UTC
Sun8
Mon9
Tue10
Wed11
Thu12
Fri13
Sat14

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

Times in Europe/Istanbul
Pzt26
Sal27
Çar28Republic Day Eve
Per29Republic Day
Cum30

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

Times in UTC
Sun8
Mon9
Tue10
Wed11
Thu12
Fri13
Sat14

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

Times in Europe/Istanbul
Tue10

America/New_York

Tuesday, November 10, 2026

Times in America/New_York
Tue10

API

Scheduler

PropTypeDefault
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 / onViewChange
The current view.
"day" | "week" | "workweek" | "month" | "list""week"
views
Views offered in the switch. Pass one view to hide the switch.
SchedulerView[]["day", "week", "month", "list"]
date / defaultDate / onDateChange
The focused day. Navigation moves by the view's length.
string (YYYY-MM-DD)today
onRangeChange
Fires when the visible days change (inclusive YYYY-MM-DD). Use it to fetch events.
({ start, end }) => void—
timeZone
Every time shown and every instant returned uses this zone. The offset is shown on the time axis.
string (IANA)LocaleProvider timeZone
slotMinutes
Grid step, and the length of keyboard-created events.
5 | 10 | 15 | 30 | 6030
snapMinutes
Drag and Alt+arrow step. Hold Alt while dragging for 5-minute steps.
number15
dayRange
The part of the day the time grid shows.
{ start: "07:00", end: "21:00" }00:00–24:00
scrollToTime
Time scrolled into view on open.
string (HH:mm)business-hours start or 08:00
height
Max height of the scrolling time grid.
string | number"36rem"
workCalendar
{ workDays, businessHours, holidays, region }. Shades non-working time, holidays and half days.
WorkCalendarfrom locale (tr → Turkish holidays)
restrictToBusinessHours
Refuses moves and new events outside business hours (red outline while dragging).
booleanfalse
onCreate
Drag on empty time, double-click, or the keyboard slot cursor (arrow keys, then Enter).
({ start, end, allDay }) => void—
onEventChange
Move or resize. Applied at once; a rejected promise reverts it and shows a toast.
({ id, start, end, allDay }) => void | Promise<void>—
onEventOpen
Click or Enter on an event.
(id, event) => void—
renderEvent
Custom event content. The accessible name stays the built-in description.
(event, { view, compact, timeLabel }) => ReactNode—
maxEventsPerDay
Month view: bars per day before “+N more”.
number3
variant
embedded drops the toolbar and frame, for cards and side panels.
"full" | "embedded""full"
toolbar
Extra toolbar content, such as filters or a “New event” button.
ReactNode—
loading
Dims events (old ones stay visible) and shows a spinner.
booleanfalse
readOnly
No create, move or resize.
booleanfalse
listDays
List view length, counted from the week start.
number7
now
A fixed “now” for tests and screenshots.
Date | number—
labels
Overrides for the scheduler namespace strings.
Partial<SchedulerLabels>—

core (React-free)

PropTypeDefault
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 / localMinutes
Wall-clock maths in an explicit zone, safe across DST.
functions—
weekDays / monthWeeks
Week and month grids from the locale's first day.
(date, firstDay) => string[] / string[][]—