Core
@hapio/calendar-core: views, events, sources, rules, callback payloads, formats, strings, zones and the store.
@hapio/calendar-core is framework-free and safe to import in Server Components.
@hapio/calendar-react re-exports all of it, so client code imports from there; server code
imports from here.
Views
function defineViews<const V extends Record<string, ViewDefinition>>(views: V): V;Declares a view map and keeps its literal names, so ViewName<typeof views> is a union of
them. Returns views itself; the first key is the default view.
function timeGrid(o?: TimeGridOptions): ViewDefinition<"timeGrid", TimeGridOptions>;A time-grid view (day columns over a time axis).
Options of timeGrid() (day columns over a time axis).
Prop
Type
function dayGrid(o?: DayGridOptions): ViewDefinition<"dayGrid", DayGridOptions>;A day-grid view (week rows of day cells).
Options of dayGrid() (week rows of day cells).
Prop
Type
function list(o?: ListOptions): ViewDefinition<"list", ListOptions>;A list view (events grouped by day).
Options of list() (events grouped by day).
Prop
Type
const defaultViews: DefaultViews;Week and day time grids, a month grid and a week list. The default view is week.
getVisibleRange
function getVisibleRange(view: ViewDefinition, o: GetVisibleRangeOptions): VisibleRangeResult;The range a view shows for a date, as onRangeChange would report it. Safe on the server and
in RSC: it never reads the clock (date: 'today' needs now, otherwise it throws a
TypeError). Throws a RangeError for an unknown view kind, an invalid view option (including
hiddenDays that hide all seven weekdays), an invalid date or an unknown zone.
getVisibleRange(defaultViews.week, { date: '2026-10-21', timeZone: 'Europe/Stockholm' })
// { startIso: '2026-10-19T00:00:00+02:00', endIso: '2026-10-26T00:00:00+01:00',
// currentStartIso: '2026-10-19', currentEndIso: '2026-10-26' }Options of getVisibleRange.
Prop
Type
The range a view shows for a date, in the shape onRangeChange reports.
startIso/endIso are true instants (start of the first and after the last visible day) as ISO
strings with offset, e.g. '2026-10-19T00:00:00+02:00'. currentStartIso/currentEndIso are
the navigation period as 'YYYY-MM-DD' dates with an exclusive end.
Prop
Type
Events
An event input carrying app data.
Prop
Type
Dates are DateInput, all-day dates PlainDateInput, times of day TimeInput and durations
DurationInput:
type DateInput = Date | number | string | InstantLike | ZonedDateTimeLike | PlainDateLike | PlainDateTimeLike;A date or date-time. How each form is read:
| Input | Meaning |
|---|---|
Date, epoch-ms number, Instant, ISO string with Z or an offset | instant |
ZonedDateTime, ISO string with [Zone] (offset optional) | instant plus source zone |
PlainDate, 'YYYY-MM-DD' | all-day (epoch days) |
PlainDateTime, offset-less 'YYYY-MM-DDTHH:mm[:ss[.sss]]' | floating: resolved in the calendar zone with 'compatible' |
type PlainDateInput = string | PlainDateLike;A calendar date: 'YYYY-MM-DD' or a PlainDate.
type TimeInput = string | PlainTimeLike;A time of day: 'HH:mm', 'HH:mm:ss', '24:00' (end of day) or a PlainTime.
type DurationInput = string | number | {
weeks?: number;
days?: number;
hours?: number;
minutes?: number;
seconds?: number;
} | DurationLike;A duration: ISO 8601 ('PT30M', 'P1D'), clock form ('00:30', '01:30:00'), a number of
milliseconds, a field object or a Temporal.Duration.
Which edits are allowed. editable: true ⇒ { move: true, resizeEnd: true, resizeStart: false, resource: true }.
Prop
Type
What callbacks receive
type CalendarEvent<TData = undefined> = TimedEvent<TData> | AllDayEvent<TData>;A timed event.
Prop
Type
An AllDayEvent has the same fields with allDay: true and start and end as
Temporal.PlainDate (the end exclusive), and no startMs or endMs.
Background events, resources and business hours
A background event: shading, a constraint target, or both.
Prop
Type
A resource (a lane).
Prop
Type
type BusinessHoursInput = boolean | BusinessHoursRule | readonly BusinessHoursRule[];true = Monday–Friday 09:00–17:00.
A weekly business-hours rule. end may be '24:00'.
Prop
Type
Sources
An async source of events (or of accessor-mode items).
- Sources are keyed by
id. The latestfetchis read from props at call time, so changing a source's identity does not refetch; a new range key,windowor zone does, and so doescommands.refetch(id). - A request superseded by a newer one (navigation, zone change, unmount,
<Activity>hidden) is aborted throughctx.signal; its result is ignored even if it resolves. windowwidens requests to whole weeks or months in the calendar zone (the reference app's "open time range"), so navigating within the window does not refetch. Default'view'.keepPrevious(defaulttrue) keeps the previous result visible while the next range loads; withfalsethe source shows nothing until its new result arrives.color,className,editableandrulesapply to every event of the source (event > source > resource > calendar).
Prop
Type
The range an event source is asked for: true instants (exclusive end) in the calendar zone,
widened by the source's window.
Prop
Type
Rules
type Constraint = "businessHours" | {
readonly group: string;
} | {
readonly start?: DateInput;
readonly end?: DateInput;
readonly days?: readonly IsoWeekday[];
readonly startTime?: TimeInput;
readonly endTime?: TimeInput;
readonly resourceIds?: readonly string[];
} | ((span: DateSpan) => boolean);Where an edit or selection may land. A list means all must hold.
'businessHours': inside business hours of the lane.{ group }: fully inside the union of background events with that group, in the same lane.- an object: inside a date range, on given weekdays, inside a daily time window, on given resources.
- a function: any test on the span.
Rules for moving, resizing and dropping events.
Prop
Type
Rules for selections.
Prop
Type
A proposed edit, as rules.event.allow sees it. kind is EventChange['kind'] or
'external'; via is EventChange['via'] or 'native'.
Prop
Type
A broken rule: why a selection (M4) or an edit (M5) cannot land where it is proposed. The
announcement and the slot cursor's name say it in words: message when allow returned a
string, else strings.ruleViolation(reason).
Prop
Type
type RuleViolationReason = "valid-range" | "not-editable" | "constraint" | "overlap" | "allow";Which rule a proposal breaks, in evaluation order (the first failure wins):
'valid-range': it reaches outsidevalidRange;'not-editable': the event's editable flags forbid the change (edits, M5; never for selections);'constraint': it is not fully inside every constraint (businessHours, a backgroundgroup, an object constraint, a function);'overlap': it overlaps an event thatoverlapdoes not allow (background events never count);'allow':allowreturnedfalseor a reason string.
Callback payloads
type DateSpan = EventTiming & {
readonly resourceId: string | null;
};A range in a lane. resourceId is always present; null outside resource lanes.
type EventTiming = {
readonly allDay: false;
readonly start: Temporal.ZonedDateTime;
readonly end: Temporal.ZonedDateTime;
readonly startIso: string;
readonly endIso: string;
} | {
readonly allDay: true;
readonly start: Temporal.PlainDate;
readonly end: Temporal.PlainDate;
readonly startIso: string;
readonly endIso: string;
};onRangeChange, onNavigate
What onRangeChange reports: the active view's range after a commit, deduplicated by
view|startIso|endIso|timeZone (exactly once per range, StrictMode included). It first fires
once the clock is known when the range depends on it.
Prop
Type
What onNavigate, onDateChange and onViewChange callers learn about a navigation.
onEventClick, onDateClick, onSelect
onEventClick payload.
Prop
Type
onDateClick payload (M4): a click or tap on an empty part of a time-grid lane (the slot
under the pointer), an all-day cell or a day cell (the day), or Enter on the slot cursor or a
day cell without a nav link. span is that slot (one snap, DST-correct, timed) or day
(all-day, exclusive end).
Prop
Type
onSelect payload (M4): a completed selection, after the rules accepted it. span is
DST-correct: timed spans are true instants with each end's own offset (Stockholm 25 October,
01:30 to 03:00 wall time: startIso 2026-10-25T01:30:00+02:00, endIso
2026-10-25T03:00:00+01:00); all-day spans are dates with an exclusive end
(2026-10-24 to 2026-10-27 for 24–26 October). span.resourceId is null until resource
views (1.3).
Prop
Type
onEventChange, onExternalDrop
A move or resize, as onEventChange receives it (M5). The
pending overlay already shows the event (and its related events) at next while the app
answers; return (or resolve) false, throw or reject to revert it.
Prop
Type
type CommitResult = void | boolean | Promise<void | boolean>;What onEventChange/onExternalDrop return. false, a throw or a rejection reverts.
An external drop (M6), as onExternalDrop receives it, with data
narrowed by acceptExternal (an item of useExternalDraggable) or nativeDrop.accept (a
native drag, such as files). Until the app answers and its events change, the calendar draws a
placeholder where the item landed: return (or resolve) false, throw or reject to take
it away again.
Prop
Type
Formats
Every format the views use; formats props take a Partial of this.
Prop
Type
Post-processing applied to formatToParts output:
omitCommas: drop literal commas ("Thu, 29 Oct" → "Thu 29 Oct").forceCommas: put a comma after the weekday where the locale has none.omitZeroMinute: "9:00 AM" → "9 AM" (only when the minute is zero).meridiem:'short'("am"),'narrow'("a") orfalse(drop the day period).
Prop
Type
type FormatFunction = (value: Temporal.ZonedDateTime | Temporal.PlainDate, ctx: FormatContext) => string;A custom format. Timed values arrive as ZonedDateTime, all-day values as PlainDate.
Strings
Every user-visible string. strings props take a Partial of this; en is built in and
@hapio/calendar-core/locales/sv is a full Swedish pack. The English default of each member
is given in its comment; the reference app's specs rely on the toolbar, view and list names.
Later milestones add members (keyboard, editing and announcement texts): M3 close, M4 the
selection texts (available … selectionCleared), M5 grabHint and the optional editing
texts (grabbed … finishChangeFirst), whose English defaults live with the editing engine
(editing()), so calendars without it never bundle them; a full pack may leave them out.
Prop
Type
The Swedish pack is sv from @hapio/calendar-core/locales/sv, the built-in English en from
@hapio/calendar-core/locales/en.
Zones and the clock
function normalizeTimeZone(id: string): string;The current IANA name of a zone id: legacy aliases map to current names
(Asia/Calcutta → Asia/Kolkata, Europe/Kiev → Europe/Kyiv), case is canonicalized and
the id is validated through Intl. 'UTC' (and its aliases such as 'Etc/UTC') gives 'UTC'.
'local' is returned unchanged: it is a sentinel the calendar resolves on the client.
Throws a RangeError for an unknown zone.
function sameTimeZone(a: string, b: string): boolean;Whether two zone ids name the same zone after normalization. Use this, never ===.
'local' equals only 'local'; an unknown id equals nothing (no throw).
function isPlainDateString(s: string): boolean;Whether s is a strict YYYY-MM-DD calendar date that round-trips (no 2026-02-30, no
extended years, no surrounding whitespace). Safe for URL-seeded dates.
A source of "now". The library never reads the wall clock itself; it asks a clock.
now() returns epoch ms. subscribe asks for a tick at least every resolutionMs
(aligned to that boundary) and returns an unsubscribe function; timers run only while
something is subscribed.
Prop
Type
function systemClock(): ClockSource;The wall clock. now() reads Date.now() at call time (so Playwright's page.clock and
Vitest fake timers work; never a Date subclass). subscribe(onTick, resolutionMs) ticks with
setTimeout aligned to the next multiple of resolutionMs (minute boundaries for 60_000);
timers are shared per resolution, ref-counted, and stop when the last subscriber leaves.
function fixedClock(at: DateInput): ClockSource;A clock stopped at at (an instant, or a floating/all-day input read as UTC). Its
subscriptions never tick.
The store
The framework-free store behind useCalendar, for bindings and tests.
function createCalendarStore<TData = undefined, TRes = undefined, const V extends ViewMap = DefaultViews, TDrag = never>(init: CalendarOptions<TData, TRes, V, TDrag>, env?: CalendarStoreEnv): CalendarStore<TData, TRes, V, TDrag>;Creates a calendar store from the initial options. Pure: no timers, listeners, fetches or
clock reads until connect()/sync(). The clock is env.clock, else options.now (a
ClockSource, or a fixed instant), else systemClock() (client only; the server snapshot has
no clock unless now is given).
const store = createCalendarStore({ events, timeZone: 'Europe/Stockholm', date: '2026-10-21' })
const model = store.resolve(options, store.getState())The framework-free calendar store. React uses it through useCalendar:
createCalendarStorehas no side effects: no timers, listeners or clock reads untilconnect().resolveis pure: the same options and state give an equal model (memoized per field identity and per layout bucket); it never emits and never reads the clock.commitruns in a layout effect and only records the committed model and the latest-options getter (hit-testing, rules and commands use them). It never emits.syncruns in a passive effect after every commit: it starts and aborts fetches, resolves the clock, firesonRangeChange(deduplicated by range key),onLoadingChangeand the navigation announcement. Idempotent, so StrictMode re-runs are harmless.connectstarts the clock subscription and returnsdisconnect; it is idempotent and ref-counted, so StrictMode and<Activity>hide/show work: disconnecting stops the clock and aborts fetches, and state is kept.
getState, getServerState, subscribe and commands are bound, so they can be passed
unbound (e.g. to useSyncExternalStore); call the other members on the store.
TData is invariant here (options both produce and consume event data), so a typed store does
not assign to CalendarStore<unknown>; untyped holders use CalendarStore<any, any, any, any>.
Prop
Type
Commands. Stable identity for the store's lifetime. Each command is
a bound function property (not a method), so destructuring and passing them as handlers is safe
(onClick={commands.next}). On a controlled value they call the matching on*Change instead of
changing state.
Prop
Type
What store.resolve returns and every hook reads: an immutable snapshot of the calendar for
one render. Fields keep their identity while their inputs are unchanged, and segments are shared
by key across rebuilds, so memoized components skip work.
Prop
Type
What store.hitTest finds under a viewport point (M6; for drops of third-party drag-and-drop
libraries such as dnd-kit or Pragmatic drag and drop): the lane (TimeGridLane.key) or day
(DayGridDay.key) and the span of the slot ('time': one snap of a lane's body, the wall
time under the point resolved 'compatible') or the day ('day-row': an all-day cell or a
day-grid cell, an all-day span of one day). 'list' is reserved: lists have no geometry and
never hit in v1.
Prop
Type