Hapio Calendar

React

@hapio/calendar-react: useCalendar, the view hooks with their prop-getters, and the types of the block's UI props.

@hapio/calendar-react renders no DOM: each hook returns data and prop-getters to spread on your elements. Every hook file carries 'use client'; the entry itself has none, so a Server Component may import its types and re-exported core functions. It re-exports @hapio/calendar-core.

The calendar

function useCalendar<TData = undefined, TRes = undefined, const V extends ViewMap = DefaultViews, TDrag = never>(options: CalendarOptions<TData, TRes, V, TDrag>, config?: UseCalendarConfig): CalendarInstance<TData, TRes, V>;

Creates and binds a calendar. TData is inferred from events (each event's data) or from eventItems + eventAccessor (the item type); V from views (default defaultViews); TDrag from acceptExternal. Share the result with CalendarProvider so parts and hooks below can read it.

const calendar = useCalendar({ events, timeZone: 'Europe/Stockholm', defaultView: 'list' })
return (
  <CalendarProvider value={calendar}>
    <div {...calendar.getRootProps({ className: 'h-[36rem]' })}>…</div>
  </CalendarProvider>
)

What useCalendar returns and CalendarProvider shares. model and getRootProps change per render; commands and store are stable for the calendar's lifetime.

Prop

Type

Second argument of useCalendar: how the caller identifies itself.

Prop

Type

function CalendarProvider(props: CalendarProviderProps): ReactNode;

Shares a useCalendar instance with the parts and hooks below. Renders only children.

function useCalendarContext(): CalendarInstance;

The calendar of the nearest CalendarProvider, untyped (unknown data, any view map). Use createCalendarKit for typed access. Throws a TypeError outside a provider.

function useCalendarSelector<T>(selector: (state: CalendarState<string>) => T, isEqual?: (a: T, b: T) => boolean): T;

Subscribes to a slice of the calendar store's state (through useSyncExternalStore, with the server snapshot during hydration) and re-renders only when the selected value changes by isEqual (default Object.is). selector must be pure; it may be an inline function. Reads the calendar from CalendarProvider.

const busy = useCalendarSelector((s) => s.live.pending.has(segment.event.key))

Typed hooks: createCalendarKit

function createCalendarKit<TData = undefined, TRes = undefined>(): <const V extends ViewMap>(views: V) => CalendarKit<TData, TRes, V>;

Typed hooks for one event type and resource type, bound to a view map, so the generics survive React context. Call at module scope.

const kit = createCalendarKit<Booking>()(defineViews({ week: timeGrid(), list: list() }))
function BookingChip({ segment }: { segment: EventSegment<Booking> }) {
  const ev = kit.useCalendarEvent(segment)
  …
}

Hooks bound to one event type, resource type and view map, so the generics survive React context. Each member behaves like the standalone hook of the same name. M3 adds useView, useTimeGrid, useDayGrid, useBackgroundEvent and useMoreLink; M6 useEventActions; M7 useRecurrenceScopePrompt. (useNowIndicator and useDayCell carry no data type and are used standalone; the @hapio/calendar-react/interaction hooks are not kit members, so the kit never bundles them.)

Prop

Type

Demo state: useLocalEvents

function useLocalEvents<TData = undefined>(initial: readonly CalendarEventInput<TData>[] | (() => readonly CalendarEventInput<TData>[])): LocalEvents<TData>;

Local event state for demos: returns [events, onEventChange], where onEventChange replaces the changed event with change.apply() and each related event (same groupId) with its copy from change.applyRelated(), matched by id; an occurrence of a server-expanded series (an input with recurrence) by its series and original start, so changing one occurrence leaves the others. Pass both to the calendar; the change settles on its own echo. Series given as series (an engine's rules) are not local state here: save change.recurrence.plan.

The toolbar

function useToolbar(): UseToolbarResult;

The toolbar of the calendar in CalendarProvider: title, labels, view options and the getters for the today/prev/next buttons, the title and the view switcher. Uses model.toolbar, model.views and the store's commands; renders nothing.

What useToolbar returns. Names follow the reference app's expectations: buttons "Previous Week"/"Next Week" (by unit), "Today" (hint "This week"), a radiogroup "Calendar view" with radios "Week view", "Day view", "Month view", "List view".

Prop

Type

Views

function useView(): ViewModelOf<ViewMap>;

The active view's model (model.view) of the calendar in CalendarProvider, discriminated by kind, for renderer dispatch and custom views. Throws a TypeError while the model is pending (render the skeleton then: model.status === 'pending'). Untyped here; the kit's useView carries the view map and data type.

Time grids

function useTimeGrid(): UseTimeGridResult;

The active time grid of the calendar in CalendarProvider: its header tiers, lanes and axis, and the getters of the time-grid markup (see UseTimeGridResult). Applies scrollTime in a layout effect and registers the scroller for commands.scrollToTime. Throws a TypeError when the active view is not a time grid or the model is pending. Events are unknown-typed here; the kit's useTimeGrid types them.

What useTimeGrid returns. Markup:

div getScrollerProps                      the view's one 2D scroller (overflow: auto)
  div getGridProps                        role=grid
    div getHeaderRowGroupProps            role=rowgroup, sticky top: 0
      div getHeaderRowProps(tier)         role=row
        div getAxisHeaderProps            role=columnheader, sticky inline-start: 0 ("Time", sr-only)
        div getColumnHeaderProps(h) ×N    role=columnheader; a nav-link button inside with navLinks
    div getAllDayRowGroupProps            role=rowgroup (with `allDay`)
      div getAllDayRowProps               role=row
        div getAllDayHeaderProps          role=rowheader ("All day"), sticky inline-start: 0
        div getAllDayCellProps(lane) ×N   role=gridcell: row segments (relative to the row), "+N more"
    div getBodyRowGroupProps              role=rowgroup
      div getBodyRowProps                 role=row
        div getAxisProps                  role=rowheader ("Time"); labels aria-hidden; sticky inline-start: 0
          div getSlotLabelProps(l) ×M
        div getLaneProps(lane) ×N         role=gridcell, position: relative, no padding-block
          bands (getBandProps), background (useBackgroundEvent), events (useCalendarEvent),
          more groups (useMoreLink), the now indicator (useNowIndicator)

Wrappers between the sticky elements and the scroller use overflow: clip, never hidden; the scroller has scrollbar-gutter: stable. Read-only time grids have no slot composite: tab stops are the header nav links (one roving stop) and the events composite. M4: with interactions.slots (the selection engine and selectable, onDateClick or droppable) each lane and all-day cell also renders the selection layer (useInteractionMirror, before the events) and the slot cursor (useSlotCursor, after them), both from @hapio/calendar-react/interaction and each scoped to its lane, so moving the cursor or extending a selection re-renders only the lanes involved; the slot composite is the third tab stop.

Prop

Type

function useNowIndicator(laneKey: string): UseNowIndicatorResult;

The now indicator of one time-grid lane (see UseNowIndicatorResult): visible when model.view.now.laneKey === laneKey. The store's clock ticks every minute while nowIndicator is on (never read during render). Returns visible: false outside time grids.

What useNowIndicator returns. It reads model.view.now (the store's clock at minute resolution with nowIndicator; never the render time).

Prop

Type

Day grids

function useDayGrid(): UseDayGridResult;

The active day grid of the calendar in CalendarProvider: its weeks and column headers, and the getters of the day-grid markup (see UseDayGridResult), including the one ResizeObserver that measures the 'auto' capacity. Throws a TypeError when the active view is not a day grid or the model is pending. Events are unknown-typed here; the kit's useDayGrid types them.

What useDayGrid returns. Markup:

div getScrollerProps                      the view's scroller
  div getGridProps                        role=grid (one ResizeObserver: the 'auto' capacity)
    div getHeaderRowGroupProps            role=rowgroup, sticky top: 0
      div getHeaderRowProps               role=row > div getColumnHeaderProps(h) ×N
    div getBodyRowGroupProps              role=rowgroup
      div getWeekRowProps(w)              role=row, position: relative (CSS grid)
        div useDayCell(d).getCellProps() ×N role=gridcell, roving tabIndex (dates composite)
          span day.label                  visually hidden: the cell's name (from content)
          button|span getWeekNumberProps(w) in the row's first cell (with `weekNumbers`)
          button|span getDayNumberProps(d)
          row segments starting here (useCalendarEvent), background, "+N more" (useMoreLink)

Tab stops: the dates composite (one roving cell) and the events composite. M4: each day cell is rendered through useDayCell(day).getCellProps() (a per-day subscription, so moves and selection steps re-render only the cells that change); with the selection engine each day cell also renders the selection layer (useInteractionMirror(day.key) from @hapio/calendar-react/interaction, row pieces positioned against the week row), and the dates composite selects (Space, arrows, Enter, Escape).

Prop

Type

function useDayCell(day: DayGridDay): UseDayCellResult;

One day cell of the active day grid (see UseDayCellResult): the dates composite's item, with a per-day subscription (its tab stop, focus and selection state), so a move or a selection step re-renders only the cells that change, never the grid or its headers. Implements the dates composite's key table itself (its moves, the nav link, onDateClick, and with the selection engine Space, Shift+moves, Enter and Escape through store.interact), so the react barrel never imports @hapio/calendar-react/interaction.

What useDayCell returns: one day-grid cell with its own subscription, so moving through the dates composite or extending a selection re-renders only the cells whose state changes (2 per move, at most 7 per week step), never the grid, its headers or the other cells.

Prop

Type

Lists

function useList(): UseListResult;

The active list view of the calendar in CalendarProvider: its days and the getters of the list markup. Throws a TypeError when the active view is not a list (renderers dispatch by model.view.kind) or the model is pending. Events are unknown-typed here; the kit's useList types them.

What useList returns. Markup: ul[aria-label="Events"] > li (day group: h{headingLevel} with the heading and alt text) > ul[aria-labelledby=heading] > li > the event (a or button, from useCalendarEvent). The list is rendered only when !isEmpty; otherwise the empty element shows emptyText. Descriptions are never inside list items (they live in the per-view hidden container), so an item's text is exactly its visible content.

Prop

Type

Events

function useCalendarEvent(segment: EventSegment, o?: UseCalendarEventOptions): UseCalendarEventResult;

One event segment's content, state and root props (see UseCalendarEventResult). One subscription per element, through the calendar's event hub (one store listener), which tells it only of changes to its own event (the pending overlay; M5: the edit session's kind and input, so a keyboard step re-renders nothing, and the touch selection), to its segment (the tab stop) or to what every event reads from the committed model, so only the affected events re-render; callbacks (onEventClick) are read from store.getOptions() when an event fires, never during render. Focus after a keyboard drop and a revert follows in useCalendar (followEvent).

What useCalendarEvent returns. M2 is the read-only subset, M3 adds the column and row variants; M5 adds editing (edits, the Space-grab keys, getResizeHandleProps(edge)). M6 adds nothing here: an event's actions trigger is useEventActions(segment).getTriggerProps of @hapio/calendar-react/interaction (a getter here would put the actions' code in every bundle).

Prop

Type

Options of useCalendarEvent.

Prop

Type

The visible content of an event.

Prop

Type

An event's state, mirrored by the root's data-* attributes. The editing states arrive with the editing engine (M5).

Prop

Type

The edits a segment allows now (useCalendarEvent().edits, M5): an engine with the 'edit' feature, the event's resolved editable flags, and where the segment is. move: Space grabs it and a drag of its body moves it (time and day grids; not in lists, not in the "+N more" popover). resizeStart/resizeEnd: the segment shows that resize handle (it holds that end of the event: isStart/isEnd; timed events in time grids, all-day events in the all-day row and day grids). All false without the engine, for recurring occurrences (until M7) and while the event's change is pending.

Prop

Type

"+N more" and background events

function useMoreLink(group: MoreGroup): UseMoreLinkResult;

One "+N more" group's trigger and popover (see UseMoreLinkResult). Subscribes per key to live.moreOpen and the events composite's focus cursor, so only the affected group re-renders.

What useMoreLink returns. The registry renders a controlled Popover (open, onOpenChange) with the trigger as PopoverTrigger (no asChild) and the listed segments through useCalendarEvent(segment, { context: 'popover' }).

Prop

Type

function useBackgroundEvent(segment: BackgroundSegment, o?: UseBackgroundEventOptions): UseBackgroundEventResult;

One background segment's root props and name (see UseBackgroundEventResult). Pure: no subscription (background segments have no live state).

What useBackgroundEvent returns.

Prop

Type

Descriptions and announcements

function useDescriptions(): UseDescriptionsResult;

The hidden descriptions container of the active view: one item per segment (segment.description, id = the event's describedById), so event elements carry only their visible text; in a selectable grid (M4) first the selection hint ("Press Space to start a selection"), which the slot cursor and the day cells refer to.

What useDescriptions returns: one hidden container per view.

Prop

Type

function useAnnouncer(): UseAnnouncerResult;

The live region of the calendar in CalendarProvider: the latest announcement (live.announcement) and the region's props. With announcer: 'auto' it speaks through element.ariaNotify (on the root) where available instead.

What useAnnouncer returns.

Prop

Type

UI props

The types of HapioCalendar's render functions, class names and renderers.

What renderEvent receives.

Prop

Type

What renderBackgroundEvent receives (M3).

Prop

Type

What renderDayHeader receives (M3).

Prop

Type

type CalendarPart = "root" | "toolbar" | "view" | "dayHeader" | "dayCell" | "dayLane" | "event" | "backgroundEvent" | "moreLink" | "nowIndicator" | "listDay" | "empty";

The parts classNames can style.

What a classNames function receives for each part. Parts rendered from M3 on (dayHeader, dayCell, dayLane, backgroundEvent, moreLink, nowIndicator) may gain fields until then.

Prop

Type

Props of a view renderer (renderers[kind], M3): the active view's model; everything else comes from the calendar context. The registry's UnsupportedView takes the same props and is rendered for a kind without a renderer (a kind newer than the copied registry files).

Prop

Type