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/rrule

Import 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

ConceptNotes
Viewsday, week, workweek, and month.
Timed eventsEvents with start and end render in the time grid. end is exclusive.
All-day eventsEvents with allDay: true render in the all-day row or as month chips.
ResourcesDay view can split the grid into lanes for workers, rooms, or locations.
InteractionsPointer and keyboard move/resize are enabled with interactive: true.
RecurrencerecurrenceRule uses the supported @schedulespark/rrule subset and is expanded inside the visible range.
TimezonetimeZone 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

OptionTypeDefaultNotes
dateDaterequiredAnchor date used to compute the visible range.
eventsCalendarEvent[]requiredControlled event list to render.
view"day" | "week" | "workweek" | "month"requiredActive 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.
compactbooleanfalseReduces grid density for tighter schedules.
defaultEventColorstringundefinedFallback CSS color for events that omit color.
hideEmptyResourcesbooleanfalseHides resource lanes with no visible events by default.
interactivebooleanfalseEnables pointer and keyboard move/resize/create behavior.
minimumDurationMinutesnumbersnapMinutesSmallest duration a resize can produce.
nowDatenew Date()Overrides the current-time indicator and today highlighting.
onDraftChange(change: CalendarDraftChange) => voidundefinedCalled after move/resize. The caller persists changes.
onEventCopy(input) => voidundefinedCalled on Ctrl/Cmd-drag copy.
onEventCreate(input) => voidundefinedCalled when empty grid space creates an event.
onEventSelect(event) => voidundefinedCalled when an event is clicked or keyboard-activated.
resourcesCalendarResource[][]Enables day-view resource lanes.
showCurrentTimeIndicatorbooleantrueShows the current-time line in time-grid views.
slotMinutesnumber60Time represented by one grid row.
snapMinutesnumberslotMinutesInteraction snap granularity.
timeZonestring"UTC"IANA timezone for recurrence expansion and local wall-clock schedules.
weekStartsOnnumber1First day of week, 0 Sunday through 6 Saturday.

Event Reference

FieldTypeNotes
idstringStable unique id. Required.
titlestringRendered event label. Required.
start / endDateRequired. end is exclusive.
resourceIdstringOptional lane assignment; must match a resource id.
colorstringOptional CSS color.
recurrenceRulestringOptional RRULE string.
recurrenceIdstringOptional occurrence identifier for overrides/exclusions.
allDaybooleanRenders outside the timed grid.
roleName, status, workerNamestringOptional display metadata for schedule apps.

Instance Methods

MethodNotes
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

PathPurpose
@schedulespark/calendarVanilla calendar API and public types.
@schedulespark/calendar/corePure date/layout helpers.
@schedulespark/calendar/reactReact wrapper around the vanilla calendar.
@schedulespark/calendar/styles.cssCalendar stylesheet.
@schedulespark/calendar/playgroundPlayground 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

SymptomCheck
Calendar is unstyledMake sure @schedulespark/calendar/styles.css is imported once.
Dragging does nothingSet interactive: true and provide onDraftChange; then call setEvents with your updated data.
Resource lanes do not appearUse view: "day", pass resources, and set event resourceId values that match resource ids.
Recurring events look shiftedPass the site IANA timezone with timeZone, especially around DST transitions.
Overlapping moves disappearCheck 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.