Calendar
A full calendar with month, week, day, agenda, timeline and year views.
SourcePackage @kerf-js/veneer
Preview
tsx
import { state } from '@kerf-js/core'import { Calendar } from '@kerf-js/veneer'import type { CalEvent } from '@kerf-js/veneer-core'export function Week() { const s = useEvents() return ( <Calendar events={s.events.value} calendars={CALENDARS} defaultView="week" defaultDate={CAL_TODAY} today={CAL_TODAY} now={CAL_NOW} locale="en-US" firstDayOfWeek={1} workingHours={[540, 1080]} onMove={s.change} onResize={s.change} onCreate={s.create} onEdit={s.edit} onDelete={s.remove} /> )}const CAL_TODAY = '2026-11-02'const CAL_NOW = '2026-11-02T10:40'const CALENDARS = [ 'Releases', 'On-call', 'Team', 'Customers',].map((name, i) => ({ id: i + 1, name,}))const EVENTS: CalEvent[] = [ { id: 'e1', title: 'Standup', start: '2026-11-02T09:30', end: '2026-11-02T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e2', title: 'Standup', start: '2026-11-03T09:30', end: '2026-11-03T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e3', title: 'Standup', start: '2026-11-04T09:30', end: '2026-11-04T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e4', title: 'Standup', start: '2026-11-05T09:30', end: '2026-11-05T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e5', title: 'Standup', start: '2026-11-06T09:30', end: '2026-11-06T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e6', title: 'Go-live', start: '2026-11-02T11:00', end: '2026-11-02T12:00', cal: 1, location: 'Room Atlas', attendees: ['Tomás Oliveira', 'Mara Lindqvist'], resource: 'atlas', }, { id: 'e7', title: 'Ranking v2', start: '2026-11-03T14:00', end: '2026-11-03T15:30', cal: 3, attendees: ['Lena Fischer', 'Noor Haddad'], video: true, resource: 'birch', }, { id: 'e8', title: 'Acme onboarding', start: '2026-11-04T10:00', end: '2026-11-04T11:00', cal: 4, video: true, attendees: ['Priya Raman'], }, { id: 'e9', title: 'Incident review: mobile-bff', start: '2026-11-04T10:30', end: '2026-11-04T11:30', cal: 2, attendees: ['Sam Okafor', 'Jun Tanaka'], resource: 'cedar', }, { id: 'e10', title: 'Rotate keys', start: '2026-11-05T13:00', end: '2026-11-05T14:00', cal: 2, locked: true, location: 'Do not move', }, { id: 'e11', title: 'Northwind', start: '2026-11-05T15:00', end: '2026-11-05T16:00', cal: 4, tentative: true, location: 'Waiting on Inés', }, { id: 'e12', title: 'Retro', start: '2026-11-06T11:00', end: '2026-11-06T12:00', cal: 1, video: true, resource: 'atlas', }, { id: 'e13', title: 'Design crit', start: '2026-11-04T09:00', end: '2026-11-04T10:00', cal: 3, resource: 'atlas', }, { id: 'e14', title: 'Hiring sync', start: '2026-11-04T13:00', end: '2026-11-04T14:30', cal: 3, resource: 'atlas', }, { id: 'e15', title: 'Billing walkthrough', start: '2026-11-04T11:00', end: '2026-11-04T12:00', cal: 4, resource: 'birch', }, { id: 'e16', title: 'Roadmap', start: '2026-11-04T15:00', end: '2026-11-04T16:30', cal: 3, resource: 'cedar', }, { id: 'a1', title: 'Jun on call', start: '2026-11-02', end: '2026-11-04', allDay: true, cal: 2, }, { id: 'a2', title: 'Ship 26.11', start: '2026-11-06', end: '2026-11-06', allDay: true, cal: 1, },]type Event = (typeof EVENTS)[number]function useEvents() { const events = state<Event[]>(EVENTS) const change = (e: Event, to: Partial<Event>) => { events.value = events.value.map((x) => x.id === e.id ? { ...x, ...to } : x, ) } return { events, change, create: (e: Event) => (events.value = [...events.value, e]), edit: (e: Event) => change(e, e), remove: (e: Event) => (events.value = events.value.filter( (x) => x.id !== e.id, )), }}Installation
npm install @kerf-js/veneerUsage
tsx
import { Calendar } from '@kerf-js/veneer'Views
Read-only month
views limits the tabs and readOnly turns off every edit.
tsx
import { Calendar } from '@kerf-js/veneer'import type { CalEvent } from '@kerf-js/veneer-core'export function Month() { return ( <Calendar events={MONTH_EVENTS} calendars={CALENDARS} views={[...COMPACT_VIEWS]} defaultView="month" defaultDate={CAL_TODAY} today={CAL_TODAY} now={CAL_NOW} locale="en-US" firstDayOfWeek={1} readOnly /> )}const CAL_TODAY = '2026-11-02'const CAL_NOW = '2026-11-02T10:40'const CALENDARS = [ 'Releases', 'On-call', 'Team', 'Customers',].map((name, i) => ({ id: i + 1, name,}))const EVENTS: CalEvent[] = [ { id: 'e1', title: 'Standup', start: '2026-11-02T09:30', end: '2026-11-02T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e2', title: 'Standup', start: '2026-11-03T09:30', end: '2026-11-03T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e3', title: 'Standup', start: '2026-11-04T09:30', end: '2026-11-04T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e4', title: 'Standup', start: '2026-11-05T09:30', end: '2026-11-05T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e5', title: 'Standup', start: '2026-11-06T09:30', end: '2026-11-06T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e6', title: 'Go-live', start: '2026-11-02T11:00', end: '2026-11-02T12:00', cal: 1, location: 'Room Atlas', attendees: ['Tomás Oliveira', 'Mara Lindqvist'], resource: 'atlas', }, { id: 'e7', title: 'Ranking v2', start: '2026-11-03T14:00', end: '2026-11-03T15:30', cal: 3, attendees: ['Lena Fischer', 'Noor Haddad'], video: true, resource: 'birch', }, { id: 'e8', title: 'Acme onboarding', start: '2026-11-04T10:00', end: '2026-11-04T11:00', cal: 4, video: true, attendees: ['Priya Raman'], }, { id: 'e9', title: 'Incident review: mobile-bff', start: '2026-11-04T10:30', end: '2026-11-04T11:30', cal: 2, attendees: ['Sam Okafor', 'Jun Tanaka'], resource: 'cedar', }, { id: 'e10', title: 'Rotate keys', start: '2026-11-05T13:00', end: '2026-11-05T14:00', cal: 2, locked: true, location: 'Do not move', }, { id: 'e11', title: 'Northwind', start: '2026-11-05T15:00', end: '2026-11-05T16:00', cal: 4, tentative: true, location: 'Waiting on Inés', }, { id: 'e12', title: 'Retro', start: '2026-11-06T11:00', end: '2026-11-06T12:00', cal: 1, video: true, resource: 'atlas', }, { id: 'e13', title: 'Design crit', start: '2026-11-04T09:00', end: '2026-11-04T10:00', cal: 3, resource: 'atlas', }, { id: 'e14', title: 'Hiring sync', start: '2026-11-04T13:00', end: '2026-11-04T14:30', cal: 3, resource: 'atlas', }, { id: 'e15', title: 'Billing walkthrough', start: '2026-11-04T11:00', end: '2026-11-04T12:00', cal: 4, resource: 'birch', }, { id: 'e16', title: 'Roadmap', start: '2026-11-04T15:00', end: '2026-11-04T16:30', cal: 3, resource: 'cedar', }, { id: 'a1', title: 'Jun on call', start: '2026-11-02', end: '2026-11-04', allDay: true, cal: 2, }, { id: 'a2', title: 'Ship 26.11', start: '2026-11-06', end: '2026-11-06', allDay: true, cal: 1, },]const MONTH_EXTRA = [ ['Q1 planning', '2026-10-27T10:00', 3], ['Acme QBR', '2026-10-29T15:00', 4], ['Ship 26.10', '2026-10-30T11:00', 1], ['Planning', '2026-11-09T10:00', 3], ['Billing v2', '2026-11-09T14:00', 4], ['Postgres 17', '2026-11-10T07:00', 2], ['Ranking v2', '2026-11-10T14:00', 3], ['Sec review', '2026-11-11T13:00', 4], ['Ship 26.11.1', '2026-11-12T11:00', 1], ['1:1 with Jun', '2026-11-12T15:30', 3], ['Design crit', '2026-11-13T10:00', 3], ['Load test', '2026-11-16T09:00', 2], ['Acme sync', '2026-11-16T13:00', 4], ['Team lunch', '2026-11-17T12:00', 3], ['Debrief', '2026-11-17T15:00', 2], ['Ship 26.11.2', '2026-11-19T11:00', 1], ['Interview', '2026-11-19T14:00', 3], ['Board prep', '2026-11-20T16:00', 4], ['Demo day', '2026-11-23T15:00', 3], ['DB failover', '2026-11-24T10:00', 2], ['Globex call', '2026-11-24T14:00', 4], ['Roadmap', '2026-11-25T11:00', 3], ['Ship 26.11.3', '2026-11-30T11:00', 1], ['Design crit', '2026-11-30T14:00', 3], ['2027 plan', '2026-12-01T10:00', 3], ['Initech pilot', '2026-12-02T13:00', 4], ['Chaos drill', '2026-12-03T09:00', 2], ['Ship 26.12.1', '2026-12-04T11:00', 1],] as constconst MONTH_DAYS = Array.from( { length: 42 }, (_, i) => new Date(Date.UTC(2026, 9, 26 + i)) .toISOString() .slice(0, 10),)const MONTH_EVENTS = [ ...EVENTS, ...MONTH_DAYS.filter( (d) => d > '2026-11-06' || d < '2026-11-02', ) .filter( (d) => [1, 2, 3, 4, 5].includes( new Date(`${d}T12:00Z`).getUTCDay(), ) && d !== '2026-11-26' && d !== '2026-11-27', ) .map((d) => ({ id: `s-${d}`, title: 'Standup', start: `${d}T09:30`, end: `${d}T09:45`, cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', })), ...MONTH_EXTRA.map(([title, start, cal], i) => ({ id: `m${i}`, title, start, end: start.slice(0, 11) + String(+start.slice(11, 13) + 1).padStart(2, '0') + start.slice(13), cal, })), { id: 'a3', title: 'Sam on call', start: '2026-11-09', end: '2026-11-15', allDay: true, cal: 2, }, { id: 'a4', title: 'Inés on call', start: '2026-11-16', end: '2026-11-22', allDay: true, cal: 2, }, { id: 'a5', title: 'Offsite', location: 'Lisbon', start: '2026-11-18', end: '2026-11-19', allDay: true, cal: 3, }, { id: 'a6', title: 'No deploys', start: '2026-11-25', end: '2026-11-25', allDay: true, cal: 1, }, { id: 'a7', title: 'US holiday', start: '2026-11-26', end: '2026-11-27', allDay: true, cal: 3, }, { id: 'a8', title: 'Jun on call', start: '2026-11-30', end: '2026-12-06', allDay: true, cal: 2, },]const COMPACT_VIEWS = ['month', 'agenda'] as constRooms timeline
resources adds the rows, and events point at one with resource. timeline sets the hours it spans.
tsx
import { state } from '@kerf-js/core'import { Calendar } from '@kerf-js/veneer'import type { CalEvent } from '@kerf-js/veneer-core'export function Rooms() { const s = useEvents() return ( <Calendar events={s.events.value} calendars={CALENDARS} resources={ROOMS} views={[...ROOM_VIEWS]} timeline={ROOM_HOURS} defaultView="timeline" defaultDate={BUSY_TODAY} today={BUSY_TODAY} now={BUSY_NOW} locale="en-US" onMove={s.change} onResize={s.change} /> )}const BUSY_TODAY = '2026-11-04'const BUSY_NOW = '2026-11-04T11:40'const CALENDARS = [ 'Releases', 'On-call', 'Team', 'Customers',].map((name, i) => ({ id: i + 1, name,}))const ROOMS = [ { id: 'atlas', name: 'Atlas', meta: '8 seats' }, { id: 'birch', name: 'Birch', meta: '6 seats' }, { id: 'cedar', name: 'Cedar', meta: '12 seats' },]const EVENTS: CalEvent[] = [ { id: 'e1', title: 'Standup', start: '2026-11-02T09:30', end: '2026-11-02T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e2', title: 'Standup', start: '2026-11-03T09:30', end: '2026-11-03T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e3', title: 'Standup', start: '2026-11-04T09:30', end: '2026-11-04T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e4', title: 'Standup', start: '2026-11-05T09:30', end: '2026-11-05T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e5', title: 'Standup', start: '2026-11-06T09:30', end: '2026-11-06T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e6', title: 'Go-live', start: '2026-11-02T11:00', end: '2026-11-02T12:00', cal: 1, location: 'Room Atlas', attendees: ['Tomás Oliveira', 'Mara Lindqvist'], resource: 'atlas', }, { id: 'e7', title: 'Ranking v2', start: '2026-11-03T14:00', end: '2026-11-03T15:30', cal: 3, attendees: ['Lena Fischer', 'Noor Haddad'], video: true, resource: 'birch', }, { id: 'e8', title: 'Acme onboarding', start: '2026-11-04T10:00', end: '2026-11-04T11:00', cal: 4, video: true, attendees: ['Priya Raman'], }, { id: 'e9', title: 'Incident review: mobile-bff', start: '2026-11-04T10:30', end: '2026-11-04T11:30', cal: 2, attendees: ['Sam Okafor', 'Jun Tanaka'], resource: 'cedar', }, { id: 'e10', title: 'Rotate keys', start: '2026-11-05T13:00', end: '2026-11-05T14:00', cal: 2, locked: true, location: 'Do not move', }, { id: 'e11', title: 'Northwind', start: '2026-11-05T15:00', end: '2026-11-05T16:00', cal: 4, tentative: true, location: 'Waiting on Inés', }, { id: 'e12', title: 'Retro', start: '2026-11-06T11:00', end: '2026-11-06T12:00', cal: 1, video: true, resource: 'atlas', }, { id: 'e13', title: 'Design crit', start: '2026-11-04T09:00', end: '2026-11-04T10:00', cal: 3, resource: 'atlas', }, { id: 'e14', title: 'Hiring sync', start: '2026-11-04T13:00', end: '2026-11-04T14:30', cal: 3, resource: 'atlas', }, { id: 'e15', title: 'Billing walkthrough', start: '2026-11-04T11:00', end: '2026-11-04T12:00', cal: 4, resource: 'birch', }, { id: 'e16', title: 'Roadmap', start: '2026-11-04T15:00', end: '2026-11-04T16:30', cal: 3, resource: 'cedar', }, { id: 'a1', title: 'Jun on call', start: '2026-11-02', end: '2026-11-04', allDay: true, cal: 2, }, { id: 'a2', title: 'Ship 26.11', start: '2026-11-06', end: '2026-11-06', allDay: true, cal: 1, },]type Event = (typeof EVENTS)[number]function useEvents() { const events = state<Event[]>(EVENTS) const change = (e: Event, to: Partial<Event>) => { events.value = events.value.map((x) => x.id === e.id ? { ...x, ...to } : x, ) } return { events, change, create: (e: Event) => (events.value = [...events.value, e]), edit: (e: Event) => change(e, e), remove: (e: Event) => (events.value = events.value.filter( (x) => x.id !== e.id, )), }}const ROOM_VIEWS = ['timeline', 'day'] as constconst ROOM_HOURS: [number, number] = [540, 1020]Overlapping meetings
Events that overlap share the column in lanes, so none hides another. Switch to Week to see the same morning beside the rest. The now line runs only through the lanes free at its minute.
tsx
import { state } from '@kerf-js/core'import { Calendar } from '@kerf-js/veneer'import type { CalEvent } from '@kerf-js/veneer-core'export function BusyDay() { const s = useEvents() return ( <Calendar events={s.events.value} calendars={CALENDARS} views={[...DAY_VIEWS]} defaultView="day" defaultDate={BUSY_TODAY} today={BUSY_TODAY} now={BUSY_NOW} locale="en-US" firstDayOfWeek={1} workingHours={[540, 1080]} onMove={s.change} onResize={s.change} /> )}const BUSY_TODAY = '2026-11-04'const BUSY_NOW = '2026-11-04T11:40'const CALENDARS = [ 'Releases', 'On-call', 'Team', 'Customers',].map((name, i) => ({ id: i + 1, name,}))const EVENTS: CalEvent[] = [ { id: 'e1', title: 'Standup', start: '2026-11-02T09:30', end: '2026-11-02T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e2', title: 'Standup', start: '2026-11-03T09:30', end: '2026-11-03T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e3', title: 'Standup', start: '2026-11-04T09:30', end: '2026-11-04T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e4', title: 'Standup', start: '2026-11-05T09:30', end: '2026-11-05T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e5', title: 'Standup', start: '2026-11-06T09:30', end: '2026-11-06T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e6', title: 'Go-live', start: '2026-11-02T11:00', end: '2026-11-02T12:00', cal: 1, location: 'Room Atlas', attendees: ['Tomás Oliveira', 'Mara Lindqvist'], resource: 'atlas', }, { id: 'e7', title: 'Ranking v2', start: '2026-11-03T14:00', end: '2026-11-03T15:30', cal: 3, attendees: ['Lena Fischer', 'Noor Haddad'], video: true, resource: 'birch', }, { id: 'e8', title: 'Acme onboarding', start: '2026-11-04T10:00', end: '2026-11-04T11:00', cal: 4, video: true, attendees: ['Priya Raman'], }, { id: 'e9', title: 'Incident review: mobile-bff', start: '2026-11-04T10:30', end: '2026-11-04T11:30', cal: 2, attendees: ['Sam Okafor', 'Jun Tanaka'], resource: 'cedar', }, { id: 'e10', title: 'Rotate keys', start: '2026-11-05T13:00', end: '2026-11-05T14:00', cal: 2, locked: true, location: 'Do not move', }, { id: 'e11', title: 'Northwind', start: '2026-11-05T15:00', end: '2026-11-05T16:00', cal: 4, tentative: true, location: 'Waiting on Inés', }, { id: 'e12', title: 'Retro', start: '2026-11-06T11:00', end: '2026-11-06T12:00', cal: 1, video: true, resource: 'atlas', }, { id: 'e13', title: 'Design crit', start: '2026-11-04T09:00', end: '2026-11-04T10:00', cal: 3, resource: 'atlas', }, { id: 'e14', title: 'Hiring sync', start: '2026-11-04T13:00', end: '2026-11-04T14:30', cal: 3, resource: 'atlas', }, { id: 'e15', title: 'Billing walkthrough', start: '2026-11-04T11:00', end: '2026-11-04T12:00', cal: 4, resource: 'birch', }, { id: 'e16', title: 'Roadmap', start: '2026-11-04T15:00', end: '2026-11-04T16:30', cal: 3, resource: 'cedar', }, { id: 'a1', title: 'Jun on call', start: '2026-11-02', end: '2026-11-04', allDay: true, cal: 2, }, { id: 'a2', title: 'Ship 26.11', start: '2026-11-06', end: '2026-11-06', allDay: true, cal: 1, },]type Event = (typeof EVENTS)[number]function useEvents() { const events = state<Event[]>(EVENTS) const change = (e: Event, to: Partial<Event>) => { events.value = events.value.map((x) => x.id === e.id ? { ...x, ...to } : x, ) } return { events, change, create: (e: Event) => (events.value = [...events.value, e]), edit: (e: Event) => change(e, e), remove: (e: Event) => (events.value = events.value.filter( (x) => x.id !== e.id, )), }}const DAY_VIEWS = ['day', 'week'] as constAgenda of the week
The same events as a list, day by day: what a phone or a quick look wants.
tsx
import { Calendar } from '@kerf-js/veneer'import type { CalEvent } from '@kerf-js/veneer-core'export function Agenda() { return ( <Calendar events={EVENTS} calendars={CALENDARS} views={[...AGENDA_VIEWS]} defaultView="agenda" defaultDate={CAL_TODAY} today={CAL_TODAY} now={CAL_NOW} locale="en-US" firstDayOfWeek={1} readOnly /> )}const CAL_TODAY = '2026-11-02'const CAL_NOW = '2026-11-02T10:40'const CALENDARS = [ 'Releases', 'On-call', 'Team', 'Customers',].map((name, i) => ({ id: i + 1, name,}))const EVENTS: CalEvent[] = [ { id: 'e1', title: 'Standup', start: '2026-11-02T09:30', end: '2026-11-02T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e2', title: 'Standup', start: '2026-11-03T09:30', end: '2026-11-03T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e3', title: 'Standup', start: '2026-11-04T09:30', end: '2026-11-04T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e4', title: 'Standup', start: '2026-11-05T09:30', end: '2026-11-05T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e5', title: 'Standup', start: '2026-11-06T09:30', end: '2026-11-06T09:45', cal: 3, recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', }, { id: 'e6', title: 'Go-live', start: '2026-11-02T11:00', end: '2026-11-02T12:00', cal: 1, location: 'Room Atlas', attendees: ['Tomás Oliveira', 'Mara Lindqvist'], resource: 'atlas', }, { id: 'e7', title: 'Ranking v2', start: '2026-11-03T14:00', end: '2026-11-03T15:30', cal: 3, attendees: ['Lena Fischer', 'Noor Haddad'], video: true, resource: 'birch', }, { id: 'e8', title: 'Acme onboarding', start: '2026-11-04T10:00', end: '2026-11-04T11:00', cal: 4, video: true, attendees: ['Priya Raman'], }, { id: 'e9', title: 'Incident review: mobile-bff', start: '2026-11-04T10:30', end: '2026-11-04T11:30', cal: 2, attendees: ['Sam Okafor', 'Jun Tanaka'], resource: 'cedar', }, { id: 'e10', title: 'Rotate keys', start: '2026-11-05T13:00', end: '2026-11-05T14:00', cal: 2, locked: true, location: 'Do not move', }, { id: 'e11', title: 'Northwind', start: '2026-11-05T15:00', end: '2026-11-05T16:00', cal: 4, tentative: true, location: 'Waiting on Inés', }, { id: 'e12', title: 'Retro', start: '2026-11-06T11:00', end: '2026-11-06T12:00', cal: 1, video: true, resource: 'atlas', }, { id: 'e13', title: 'Design crit', start: '2026-11-04T09:00', end: '2026-11-04T10:00', cal: 3, resource: 'atlas', }, { id: 'e14', title: 'Hiring sync', start: '2026-11-04T13:00', end: '2026-11-04T14:30', cal: 3, resource: 'atlas', }, { id: 'e15', title: 'Billing walkthrough', start: '2026-11-04T11:00', end: '2026-11-04T12:00', cal: 4, resource: 'birch', }, { id: 'e16', title: 'Roadmap', start: '2026-11-04T15:00', end: '2026-11-04T16:30', cal: 3, resource: 'cedar', }, { id: 'a1', title: 'Jun on call', start: '2026-11-02', end: '2026-11-04', allDay: true, cal: 2, }, { id: 'a2', title: 'Ship 26.11', start: '2026-11-06', end: '2026-11-06', allDay: true, cal: 1, },]const AGENDA_VIEWS = ['agenda', 'week'] as constWhen to use
Use it for
- People read and change a schedule of events
- Events move, resize or get created by dragging
Reach for something else
- InlineCalendar, when people only pick a day
- Agenda, when a read-only list of what is next is enough
Accessibility
- Tab
- Moves between the toolbar, the views and the events
- M
- On a focused event: starts moving it with the arrow keys
- Enter
- Opens the focused event
- Every drag has a keyboard route, and the event editor can change the same fields without dragging.
- Pass
todayandnowto keep the server and browser in step. Without them they arrive after the page loads.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
events | CalEvent[] | — | The events to show. You own this list. |
calendars | CalendarSource[] | — | The named sources in the side rail. |
view | CalView | State<CalView> | — | The view showing: a State to share it, or a value. |
defaultView | CalView | — | The view it opens on when view isn't given. Week without either. |
views | CalView[] | — | The views offered as tabs, in this order. All six without it. |
date | string | State<string> | — | The date the view is on (2026-11-04): a State to share it, or a value. |
defaultDate | string | — | The date it opens on when date isn't given. Today without either. |
timeZone | string | State<string | null> | — | The toolbar chip's zone. Without one, wall times show as they are. |
zones | { label: string; zone: string; }[] | — | Secondary zones in the rail. |
today | string | — | Today's date (2026-11-02), marked in every view. The browser's date after mount without it, so hydration writes nothing. |
now | string | — | The now line's wall time; the browser's clock after mount without it. |
locale | string | — | The locale for day and month names, times and the first day of the week. The browser's without it. |
hourCycle | string | — | h12 or h23 to force a clock; the locale's without it. |
firstDayOfWeek | number | — | The week's first day, 0 for Sunday to 6 for Saturday. The locale's without it. |
snap | number | — | Minutes a drag or resize snaps to. 15 without it. |
workingHours | [number, number] | — | The start and end of the working day, in minutes from midnight: [540, 1080] is 09:00 to 18:00. The rest is shaded. |
live | LiveState | — | The toolbar LiveStatus. |
resources | Resource[] | — | Timeline rows; events point at one with resource. |
timeline | [number, number] | — | The hours the timeline spans, in minutes from midnight. |
counts | Record<string, number> | — | Year view's counts; from events without it. |
readOnly | boolean | false | Turns off dragging, resizing, creating and the keyboard move mode. Events still open their popover. |
| Event | Type | Default | Description |
|---|---|---|---|
onViewChange | ((v: CalView) => void) | — | Hears every change of view, by tab, key or the mini month. |
onDateChange | ((d: string) => void) | — | Hears the new date when the arrows, Today, the mini month or a drill-in move it. |
onTimeZoneChange | ((z: string | null) => void) | — | Hears a zone picked from the toolbar chip, or null for wall times. |
onMove | ((e: CalEvent, to: EventChange, reason: Reason) => void | Promise<unknow… | — | Called with the event and its new times. Return a rejected promise to put it back. |
onResize | ((e: CalEvent, to: EventChange, reason: Reason) => void | Promise<unknow… | — | An event's edge dragged to a new time. A rejected promise puts it back and announces why. |
onCreate | ((e: CalEvent) => void) | — | A new event saved from the editor, after a drag over empty time, a press on a slot or New event. |
onEdit | ((e: CalEvent, scope?: RecurrenceScope | undefined) => void) | — | An event saved from the editor; scope says which of a repeating event's occurrences. |
onDelete | ((e: CalEvent, scope?: RecurrenceScope | undefined) => void) | — | An event deleted after its confirmation; scope says which of a repeating event's occurrences. |
onOpen | ((e: CalEvent) => void) | — | An event's popover opened, by press or Enter. |
onJoin | ((e: CalEvent) => void) | — | Join pressed in the popover of an event with a video link. |
Parts
Every element carries a stable name, so you can style it from your own CSS: [data-scope="calendar"] [data-part="root"]
rootlayout