Calendar
Use @schedulespark/calendar for framework-free schedule calendar layouts and interactions.
Calendar
@schedulespark/calendar renders day, week, work-week, month, all-day, recurrence, and resource lane layouts with no framework dependency.
Related terms: npm, ScheduleSpark, @schedulespark/calendar, calendar, schedule calendar, scheduler, scheduling, shift scheduling, resource scheduling, resource lanes, work week, week view, month view, all-day events, drag and drop, resize events, recurrence, vanilla JavaScript, framework-free, TypeScript.
Install
npm install @schedulespark/calendar @schedulespark/rruleImport the stylesheet once in your app shell:
import "@schedulespark/calendar/styles.css";The package has one runtime workspace dependency, @schedulespark/rrule, for recurrence expansion. It has no React peer dependency and can be mounted from Vanilla, React, Angular, Vue, or any other framework.
Basic Usage
import { createCalendar } from "@schedulespark/calendar";
import "@schedulespark/calendar/styles.css";
const host = document.querySelector("#calendar");
if (!(host instanceof HTMLElement)) {
throw new Error("Missing #calendar host");
}
const calendar = createCalendar({
date: new Date("2026-07-06T12:00:00.000Z"),
view: "week",
events: [
{
id: "shift-1",
title: "Morning shift",
start: new Date("2026-07-06T08:00:00.000Z"),
end: new Date("2026-07-06T10:00:00.000Z")
}
]
});
calendar.mount(host);The calendar is controlled. It renders the events you pass in and exposes callbacks for user actions; it does not mutate or store your schedule data for you.
React
React consumers can import ScheduleCalendar from @schedulespark/calendar/react. It hosts the same framework-free calendar instance and updates it through setOptions when props change.
import { useState } from "react";
import { ScheduleCalendar } from "@schedulespark/calendar/react";
import "@schedulespark/calendar/styles.css";
import type { CalendarDraftChange, CalendarEvent } from "@schedulespark/calendar";
const initialEvents: CalendarEvent[] = [
{
id: "shift-1",
title: "Morning shift",
start: new Date("2026-07-06T08:00:00.000Z"),
end: new Date("2026-07-06T10:00:00.000Z")
}
];
export function Schedule(): JSX.Element {
const [events, setEvents] = useState(initialEvents);
function applyDraft(change: CalendarDraftChange): void {
setEvents((current) =>
current.map((event) =>
event.id === change.eventId ? { ...event, start: change.start, end: change.end, resourceId: change.resourceId } : event
)
);
}
return (
<ScheduleCalendar
date={new Date("2026-07-06T12:00:00.000Z")}
events={events}
interactive
onDraftChange={applyDraft}
view="week"
/>
);
}The wrapper does not import CSS for you. Keep @schedulespark/calendar/styles.css in your app shell or route bundle.
Core Concepts
| Concept | Notes |
|---|---|
| Views | day, week, workweek, and month. |
| Timed events | Events with start and end render in the time grid. end is exclusive. |
| All-day events | Events with allDay: true render in the all-day row or as month chips. |
| Resources | Day view can split the grid into lanes for workers, rooms, or locations. |
| Interactions | Pointer and keyboard move/resize are enabled with interactive: true. |
| Recurrence | recurrenceRule uses the supported @schedulespark/rrule subset and is expanded inside the visible range. |
| Timezone | timeZone controls local wall-clock recurrence expansion and display. Defaults to UTC. |
Resource Lanes
Use resource lanes when one day needs columns for workers, rooms, stations, trucks, or locations.
const calendar = createCalendar({
date: new Date("2026-07-06T12:00:00.000Z"),
view: "day",
resources: [
{ id: "worker-1", title: "Ada", subtitle: "Front desk" },
{ id: "worker-2", title: "Grace", subtitle: "Kitchen" }
],
events: [
{
id: "shift-1",
title: "Morning shift",
start: new Date("2026-07-06T08:00:00.000Z"),
end: new Date("2026-07-06T12:00:00.000Z"),
resourceId: "worker-1"
}
]
});If hideEmptyResources is true, resource lanes with no events in the current visible range are hidden by default. Manual show/hide controls override the automatic default.
Controlled Interactions
import { createCalendar, type CalendarDraftChange, type CalendarEvent } from "@schedulespark/calendar";
import "@schedulespark/calendar/styles.css";
const host = document.querySelector("#calendar");
if (!(host instanceof HTMLElement)) {
throw new Error("Missing #calendar host");
}
let events: CalendarEvent[] = [
{
id: "shift-1",
title: "Morning shift",
start: new Date("2026-07-06T08:00:00.000Z"),
end: new Date("2026-07-06T10:00:00.000Z"),
resourceId: "worker-1"
}
];
function applyDraft(change: CalendarDraftChange): void {
events = events.map((event) =>
event.id === change.eventId
? { ...event, start: change.start, end: change.end, resourceId: change.resourceId }
: event
);
calendar.setEvents(events);
}
const calendar = createCalendar({
date: new Date("2026-07-06T12:00:00.000Z"),
events,
interactive: true,
onDraftChange: applyDraft,
resources: [{ id: "worker-1", title: "Ada" }],
view: "day"
});
calendar.mount(host);When collisionPolicy is "allow", overlapping drafts are emitted with hasCollisionWarning: true. When it is "reject-overlap", overlapping drafts are suppressed.
Creating And Copying Events
Set onEventCreate to support click or drag creation from empty grid space. Set onEventCopy to support Ctrl/Cmd-drag duplication of existing events.
const calendar = createCalendar({
date: new Date("2026-07-06T12:00:00.000Z"),
view: "week",
interactive: true,
events,
onEventCreate(input) {
events = [
...events,
{
id: crypto.randomUUID(),
title: "New shift",
start: input.start,
end: input.end,
resourceId: input.resourceId
}
];
calendar.setEvents(events);
},
onEventCopy(input) {
events = [
...events,
{
...input.sourceEvent,
id: crypto.randomUUID(),
start: input.start,
end: input.end,
resourceId: input.resourceId
}
];
calendar.setEvents(events);
}
});Recurring Events
const calendar = createCalendar({
date: new Date("2026-07-06T12:00:00.000Z"),
view: "week",
timeZone: "America/New_York",
events: [
{
id: "standup",
title: "Team standup",
start: new Date("2026-07-06T09:00:00.000Z"),
end: new Date("2026-07-06T09:30:00.000Z"),
recurrenceRule: "FREQ=WEEKLY;BYDAY=MO,WE,FR"
}
]
});Recurring events are expanded only for the visible range. Unsupported RRULE fields are ignored by the current beta parser; see RRule for the supported subset.
Options Reference
| Option | Type | Default | Notes |
|---|---|---|---|
date | Date | required | Anchor date used to compute the visible range. |
events | CalendarEvent[] | required | Controlled event list to render. |
view | "day" | "week" | "workweek" | "month" | required | Active layout. |
businessHours | { start: string; end: string } | { start: "00:00", end: "24:00" } | Inclusive/exclusive HH:mm bounds for the time grid. Use 24:00 for end of day. |
collisionPolicy | "allow" | "reject-overlap" | "allow" | Controls whether overlap drafts are emitted or rejected. |
compact | boolean | false | Reduces grid density for tighter schedules. |
defaultEventColor | string | undefined | Fallback CSS color for events that omit color. |
hideEmptyResources | boolean | false | Hides resource lanes with no visible events by default. |
interactive | boolean | false | Enables pointer and keyboard move/resize/create behavior. |
minimumDurationMinutes | number | snapMinutes | Smallest duration a resize can produce. |
now | Date | new Date() | Overrides the current-time indicator and today highlighting. |
onDraftChange | (change: CalendarDraftChange) => void | undefined | Called after move/resize. The caller persists changes. |
onEventCopy | (input) => void | undefined | Called on Ctrl/Cmd-drag copy. |
onEventCreate | (input) => void | undefined | Called when empty grid space creates an event. |
onEventSelect | (event) => void | undefined | Called when an event is clicked or keyboard-activated. |
resources | CalendarResource[] | [] | Enables day-view resource lanes. |
showCurrentTimeIndicator | boolean | true | Shows the current-time line in time-grid views. |
slotMinutes | number | 60 | Time represented by one grid row. |
snapMinutes | number | slotMinutes | Interaction snap granularity. |
timeZone | string | "UTC" | IANA timezone for recurrence expansion and local wall-clock schedules. |
weekStartsOn | number | 1 | First day of week, 0 Sunday through 6 Saturday. |
Event Reference
| Field | Type | Notes |
|---|---|---|
id | string | Stable unique id. Required. |
title | string | Rendered event label. Required. |
start / end | Date | Required. end is exclusive. |
resourceId | string | Optional lane assignment; must match a resource id. |
color | string | Optional CSS color. |
recurrenceRule | string | Optional RRULE string. |
recurrenceId | string | Optional occurrence identifier for overrides/exclusions. |
allDay | boolean | Renders outside the timed grid. |
roleName, status, workerName | string | Optional display metadata for schedule apps. |
Instance Methods
| Method | Notes |
|---|---|
mount(host) | Renders into an HTMLElement. |
setDate(date) | Updates the anchor date and re-renders. |
setEvents(events) | Replaces the controlled event list. |
setView(view) | Switches layout. |
setOptions(options) | Merges partial options and re-renders. |
destroy() | Clears the mounted DOM. |
Export Paths
| Path | Purpose |
|---|---|
@schedulespark/calendar | Vanilla calendar API and public types. |
@schedulespark/calendar/core | Pure date/layout helpers. |
@schedulespark/calendar/react | React wrapper around the vanilla calendar. |
@schedulespark/calendar/styles.css | Calendar stylesheet. |
@schedulespark/calendar/playground | Playground helpers. |
Accessibility And Keyboard
Interactive events can be focused and activated with Enter or Space. When interactive is enabled, keyboard move/resize follows the same controlled onDraftChange flow as pointer interactions. Event labels should be meaningful because they are exposed as the primary accessible name.
Troubleshooting
| Symptom | Check |
|---|---|
| Calendar is unstyled | Make sure @schedulespark/calendar/styles.css is imported once. |
| Dragging does nothing | Set interactive: true and provide onDraftChange; then call setEvents with your updated data. |
| Resource lanes do not appear | Use view: "day", pass resources, and set event resourceId values that match resource ids. |
| Recurring events look shifted | Pass the site IANA timezone with timeZone, especially around DST transitions. |
| Overlapping moves disappear | Check whether collisionPolicy is "reject-overlap". |
Lifecycle
Use mount(host) to render, setDate(date), setEvents(events), and setView(view) to update, and destroy() when removing the calendar from the page.