People, rooms or equipment as rows and days or hours as columns, holding blocks: shifts, absences, bookings, allocations. It has a template palette, rule warnings (never hard blocks), a working calendar with holidays, a one-code-per-cell variant for attendance sheets, and a full keyboard grid. Every drag has a menu or key alternative.
November 9 – 15, 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 { ResourceCalendar, minRestRule } from "@vamos-ui/react/scheduler";
<ResourceCalendar
resources={staff} // { id, title, avatarUrl?, group?, meta? }
blocks={shifts} // { id, resourceId, start, end, label?, tone?, status? }
range={{ start: "2026-11-09", end: "2026-11-15" }}
onRangeChange={setRange}
templates={[{ id: "am", label: "Morning", start: "07:00", end: "15:00", tone: "chart-2" }]}
rules={[minRestRule(11 * 60)]}
onBlocksChange={({ created, updated, deleted }) => api.shifts.sync({ created, updated, deleted })}
/>Keyboard: the grid has one tab stop. Use the arrow keys to move, Shift+arrows to select, Enter for the cell menu (assign template, remove, your actions) and Delete to clear (with Undo). Alt+arrows move the cell's blocks. In codes mode, type a code.
Pointer: drag across cells to select. Click a palette template to arm it, then click or drag over cells to paint. You can also drag a template or a block onto a cell. Every drag has a menu or keyboard path (WCAG 2.5.7).
Blocks are opaque colour mixes and never blur. Only the outer surface uses the glass material.
Examples
Leave plan (month)
HR leave plan: multi-day blocks run across cells. Pending is striped, approved is solid, rejected is hidden unless you show it, and a half day takes half a cell. The cell menu has a custom “Request time off…” action.
November 1 – 30, 2026
Rules: rest hours and weekly cap
Rules are plain objects. Here they use Turkish labour-law values as an example dataset: 11 h rest, a 45 h week, at most one person off per day. Problems get a corner flag and are part of the cell's accessible name. Team rules flag the day header.
Rooms × hours
unit="hour" makes each column an hour slot, for spa or meeting-room booking. Blocks span slots by their real start and end.
Tuesday, November 10, 2026
Attendance codes
variant="codes" puts one short code in each cell, for attendance sheets (the Time tracker recipe). Type a code to set it, Shift+arrows select a range, Delete clears it (with Undo).
Focus a cell and type W, R, L or S to set it. Shift + arrows select a range, Delete clears it.
November 2 – 15, 2026
300 people
Rows are virtualized from 60 resources (virtualizeAt). The header and row names stay sticky.
API
ResourceCalendar
| Prop | Type | Default |
|---|---|---|
resources{ id, title, avatarUrl?, group?, meta?, readOnly? }. meta is the second line, such as “Head chef” or “9 days left”. | Resource[] | — |
blocks{ id, resourceId, start, end, label?, code?, kind?, tone?, status?, halfDay?, editable?, note? }. In day mode, YYYY-MM-DD dates (inclusive) or UTC ISO instants for timed shifts. In hour mode, UTC ISO instants. | ResourceBlock[] | — |
rangeVisible days, inclusive YYYY-MM-DD. | { start, end } | — |
onRangeChangeShows Today, ‹ and › in the toolbar. Steps by the range length, or by month when the range is a whole month. | (range) => void | — |
unitDay columns, or hour slots of each day. | "day" | "hour" | "day" |
hoursHour mode: the visible hours. | { start: "08:00", end: "20:00" } | 08:00–20:00 |
variantcodes puts one short code in each cell. Assigning replaces what's there. | "blocks" | "codes" | "blocks" |
templates{ id, label, start?, end?, tone?, code?, kind?, shortcut?, status? }. Shown as the palette and in the cell menu. With start and end the template makes a timed shift (overnight works). | ResourceTemplate[] | — |
rules{ id, check?(ctx), checkTeam?(ctx) } returning { blockId? | day?, message, severity }. Rules only warn. Built-in factories: minRestRule, maxHoursRule, maxConcurrentRule. | ResourceRule[] | — |
workCalendarWork days and holidays (tr → Turkish public holidays). Weekend and holiday columns are shaded. | WorkCalendar | from locale |
onBlocksChangeEvery edit in one change set. Applied at once; a rejected promise reverts it. Deletes offer Undo. | ({ created?, updated?, deleted? }) => void | Promise<void> | — |
cellActions / onCellActionExtra cell-menu items, such as “Request time off”. Receives the selected cells. | { id, label, tone? }[] / ({ id, cells }) => void | — |
onBlockOpenAdds the cell's blocks to the cell menu as items that open them. | (block) => void | — |
renderBlockCustom block content. | (block, { compact }) => ReactNode | — |
summaryThe sticky summary row. | ((day, blocks) => ReactNode) | false | headcount |
rowSummaryA trailing value in each row header, such as “38 h”. | (resource, blocks) => ReactNode | — |
groupByGroup rows by resource.group, with a header row for each group. | "group" | null | null |
showRejectedShow rejected blocks, struck through. | boolean | false |
readOnly / loadingNo edits / skeleton rows (or dimmed rows while reloading). | boolean | false |
timeZoneZone for timed blocks and templates. | string | LocaleProvider timeZone |
height / virtualizeAtScroll height, and the resource count from which rows are virtualized. | string | number / number | "32rem" / 60 |
toolbarExtra toolbar content. | ReactNode | — |
labelsOverrides for the resourceCalendar namespace strings. | Partial<ResourceCalendarLabels> | — |
Rule factories
| Prop | Type | Default |
|---|---|---|
minRestRule(minutes, message?)Warns when two timed blocks of one person are closer than minutes apart. | ResourceRule | — |
maxHoursRule(minutes, message?)Danger when a person's timed blocks in the range go over the cap. | ResourceRule | — |
maxConcurrentRule(max, kinds, message?)Flags days on which more than max people have a block of those kinds. | ResourceRule | — |