Configuration

Documentation index

Options and plugins

In vanilla code, pass options inside createCalendar(host, { plugins, options }). In React, pass options directly to <Calendar> as props. Plugins are selected at creation time. To change plugins, destroy/recreate the vanilla instance or remount React with a new key.

Set view explicitly to make the starting layout independent of plugin order.

PluginView names
DayGriddayGridDay, dayGridWeek, dayGridMonth
TimeGridtimeGridDay, timeGridWeek
ListlistDay, listWeek, listMonth, listYear
ResourceTimeGridresourceTimeGridDay, resourceTimeGridWeek
ResourceTimelineresourceTimelineDay, resourceTimelineWeek, resourceTimelineMonth, resourceTimelineYear
InteractionAdds date selection/clicks, event dragging/resizing, and the hover pointer; no view of its own

Options resolve from base defaults → plugin defaults → inherited view defaults → your global options → your per-view overrides. theme, buttonText, icons, and customButtons shallow-merge; other options replace the previous value. TypeScript's CalendarOptions is the complete input contract.

Setting an option to undefined (or dropping a React prop) restores its default. views is replaced as a whole: view names, types, and per-view overrides missing from the new map reset.

Toolbars and custom views

Spaces separate button groups; commas join buttons in a group. Use actual view names in the toolbar. Set headerToolbar={{ start: '', center: '', end: '' }} to hide the built-in toolbar.

import 'z-cal/polyfill'
import { Calendar, DayGrid, TimeGrid } from 'z-cal/react'
import 'z-cal/style.css'

const plugins = [DayGrid, TimeGrid]
const views = {
  threeDays: { type: 'timeGridWeek', duration: { days: 3 }, buttonText: { threeDays: '3 days' } },
}

export function Schedule() {
  return (
    <Calendar
      plugins={plugins}
      view="threeDays"
      views={views}
      firstDay={1}
      hiddenDays={[0, 6]}
      height="650px"
      slotMinTime="08:00"
      slotMaxTime="18:00"
      scrollTime="08:00"
      slotDuration="00:30"
      headerToolbar={{ start: 'title', end: 'today prev,next dayGridMonth,timeGridWeek,threeDays' }}
    />
  )
}

firstDay and hiddenDays use Sunday 0 through Saturday 6. Durations accept objects such as { days: 3 }, ISO durations such as 'PT30M', time strings such as '00:30', or numbers in seconds. dateIncrement overrides how far previous/next navigation advances; otherwise the view duration controls it. Per-view type must name a view provided by a registered plugin or another custom view.

Height and responsive weeks

Use a CSS length string for height, such as '650px', '80vh', or '100%'. Percentage height needs a defined parent height; in React the host wrapper also needs to fill that parent (for example, style={{ height: '100%' }}). Without a height, the calendar sizes to its content.

Timed collisions use staggered cards for small clusters and separate strips for dense clusters. Click a day header to widen that date within the week. Hover or keyboard-focus a card for its full details; see overlapping timed events.

Month weeks fill available height. Crowded weeks may grow to display all events. Use dayMaxEvents: true for uniform rows with a “+N more” popover, or a number to limit visible events.

import 'z-cal/polyfill'
import { Calendar, TimeGrid } from 'z-cal/react'
import 'z-cal/style.css'

const plugins = [TimeGrid]
export function ResponsiveWeek() {
  return (
    <Calendar
      plugins={plugins}
      view="timeGridWeek"
      height="650px"
      responsiveWeek={{ minDayWidth: 160 }}
    />
  )
}

responsiveWeek defaults to false. true uses a 160px minimum day width and reserves 80px for the time axis. It fits 1–7 calendar days inside the calendar container, up to the configured 1–7 day duration. Hidden days are still excluded. A positive minDayWidth customizes that threshold.

Shortened ranges start on the selected date; previous/next move by that displayed range unless dateIncrement overrides it. Full weeks restore firstDay alignment. Resizing preserves the configured date and duration; getView() reports the actual range. Hidden/zero-width containers retain their last usable measurement until they become visible.

This behavior applies to timeGridWeek and custom views inheriting it. It does not adapt month, list, resource time-grid, or timeline views.

Swipe between periods

Enable swipeNavigation: true to move to the previous or next period with a horizontal trackpad scroll or a one-finger touch swipe. It defaults to false. A Shift+mouse-wheel gesture is also supported. The demo includes a Swipe to navigate checkbox.

import { Calendar, DayGrid, TimeGrid } from 'z-cal/react'

export function SwipeCalendar() {
  return <Calendar plugins={[DayGrid, TimeGrid]} view="timeGridWeek" swipeNavigation />
}

Navigation uses the same increment as the previous/next buttons: day, week, month, a custom dateIncrement, or the current responsive duration. Right-to-left layouts reverse the gesture mapping. The calendar body follows the gesture with a rubber-band offset and dims slightly while a compact chip names the destination period. Crossing the threshold commits the new date and pushes the periods across as one strip over 360ms: an inert snapshot of the outgoing body slides out while the new period slides in beside it, and the chip fades as it settles. Releasing early springs back (180ms). No adjacent calendar views are rendered; the snapshot is the only extra DOM, and it is removed when the push ends.

A trackpad or Shift+wheel gesture pages at most once. Direction is established from accumulated travel, so tiny axis or reversal jitter is ignored, and the momentum tail that follows a swipe does not page again. A reversal, or a fresh swipe that clearly outgrows the decaying tail, starts a new gesture without waiting for the tail to end; one that lands while the previous period is still leaving runs next. Residual travel is discarded when input stops, so stopping cannot add a navigation step. Touch swipes use distance and velocity on release: a short flick completes and a slow short drag springs back.

When a swipe loads a new range, events from the previous response stay hidden until loading finishes, then the current results fade in over 160ms. With fetchPadding enabled, adjacent periods are already cached, so the new period usually arrives with its events in place. An unavailable destination gives dimmed feedback without navigating. Reduced-motion preferences remove the card and chip movement: the period swaps as soon as the gesture commits.

Once a gesture is horizontal it is axis-locked: small vertical drift and the momentum tail stay with the calendar instead of scrolling the page sideways, and page scrolling elsewhere does not cancel it. Predominantly vertical input hands the gesture back to scrolling. The calendar body sets touch-action: pan-y pinch-zoom while it has no horizontal overflow of its own, so a horizontal touch never starts a page pan. Vertical scrolling, zoom gestures, form controls, popovers, and active event drags retain their normal behavior. Scrollable horizontal content takes priority: a wide resource grid or timeline continues to scroll within its existing columns, including at its edges. Use the navigation buttons there, or disable paging per view through views. Touch paging cancels a pending long press only when horizontal intent is established; an event drag already in progress keeps control.

Paging respects validRange. Time grids keep their vertical scroll position whenever only the date changes, by swipe or by the previous/next buttons; scrollTime applies when the view mounts or when the option or time zone changes, and scrollToFirstEvent repositions each period. The existing buttons remain available for keyboard navigation. Theme key swipeChip controls the destination chip's class, and the calendar root carries data-cx-swipe (tracking, settling, or pushing) while a swipe is live, for styling hooks.

Keyboard navigation and shortcuts

Enable keyboardNavigation: true to move through the calendar with the keyboard. It defaults to false. One day cell holds the Tab stop: in a month or list view the day, in a time grid the column of a date, in a timeline the date cell of a resource row. The arrow keys move it, and in time grids and timelines a slot cursor shows which time is focused. Tab moves from the focused cell to that date's events and its +N more link, then out of the calendar. The calendar root becomes a grid and cells gridcells with a full date label (dayCellAriaLabelFormat, default { dateStyle: 'full' }), so assistive technology announces where the focus is.

import { Calendar, DayGrid, TimeGrid, Interaction } from 'z-cal/react'

export function KeyboardCalendar() {
  return (
    <Calendar
      plugins={[DayGrid, TimeGrid, Interaction]}
      view="timeGridWeek"
      keyboardNavigation
      keyboardShortcuts
      selectable
    />
  )
}
KeyDay grid and listTime gridTimeline
Left / Rightprevious / next dayprevious / next columnprevious / next slot, crossing into the next date
Up / Downprevious / next row (list: previous / next day)previous / next slot; Up from the first slot enters the all-day row, Down returnsprevious / next resource row
Home / Endfirst / last cell of the rowfirst / last columnfirst / last cell of the row
Ctrl+Home / Ctrl+Endfirst / last visible datefirst / last visible slotfirst / last slot of the row
PageUp / PageDownprevious / next periodsamesame
Enter / SpacedateClick, or complete a pending selectionsame, with the focused timesame
Shift+Arrowpreview a selection from the anchorsamesame
Escapecancel the pending selection, else unselect(), else leave the gridsamesame
Escape on an eventreturn to the event's cellsame, closing its previewsame
Up / Down on an eventprevious / next event of the focused datesamesame

Moving past the first or last visible date pages to the adjacent period and lands on its edge; PageUp and PageDown keep the date offset. Paging respects validRange like the toolbar buttons. Right-to-left layouts swap Left and Right. Shift+Arrow needs Interaction and selectable; the preview runs selectConstraint, Enter fires select with the KeyboardEvent as jsEvent, and a selection never extends past the visible range. Keyboard dateClick works without Interaction. The Tab stop follows toolbar, swipe, and API navigation: a focused cell keeps focus under its new date, and a date that left the view moves to the nearest visible one. calendar.focus(target?) moves keyboard focus to a date, all-day cell, time (time: '09:30'), or resource from your own controls.

Enable keyboardShortcuts: true for Google Calendar's single keys: t today, j / n next, k / p previous, and d, w, m, a for the day, week, month, and agenda views that are registered (each alias tries the time grid, day grid, resource, and list variants in that order). Pass { keys, target } to replace the map or to listen on document instead of the calendar root:

import type { CalendarOptions } from 'z-cal'

export const options: CalendarOptions = {
  keyboardShortcuts: {
    keys: {
      t: 'today',
      ArrowRight: 'next',
      ArrowLeft: 'prev',
      g: 'focus',
      l: { view: 'listWeek' },
    },
    target: document,
  },
}

Actions are today, prev, next, focus (move focus into the grid), { view } with a registered view name or alias, or a function receiving { key, jsEvent, view }. Shortcuts never fire while a text field, select, or editable region has focus, while Ctrl, Meta, or Alt is held, or during a pointer gesture. With the default root target, keys only apply while focus is inside the calendar. Keep a shortcut object stable across React renders, or rely on the content comparison: equal maps do not re-attach the listener.

A polite live region announces the new title after every navigation and view change, and the focused time or pending selection while moving with the keyboard (announceNavigation, default true). Disable it when your page has its own live title. Theme keys keyboardCursor and liveRegion name the cursor and region classes, and the root carries data-cx-keyboard while a cell has focus, for styling hooks.

Scroll to the first event

Set scrollToFirstEvent: true to initially scroll a time-grid or resource time-grid to its earliest visible timed event. It defaults to false. In a multi-day view, the earliest clock time across all visible days determines the shared scroll position. Events continuing into the window count; all-day and background events do not.

import { Calendar, TimeGrid } from 'z-cal/react'

export function FirstEventCalendar() {
  return <Calendar plugins={[TimeGrid]} scrollToFirstEvent scrollTime="08:00" />
}

The calendar waits for asynchronous event sources to finish. Empty windows use scrollTime (default 06:00); if their first event arrives later, the calendar scrolls to it. The position is clamped to the visible slot range, with a little space above the event where possible.

Automatic positioning happens once per displayed range. Scrolling manually or using the calendar's scroll API takes precedence, including while events are loading. Editing or refetching events after the initial scroll does not move you. Navigation or toggling this option starts a fresh initial scroll. The demo includes a Scroll to first event checkbox for trying this behavior.

Fill the time-grid height

Set expandRows: true to stretch time slots across the calendar's available height in time-grid and resource time-grid views. It defaults to false; existing fixed slot sizing is unchanged.

import type { CalendarOptions } from 'z-cal'

const options: CalendarOptions = {
  view: 'timeGridWeek',
  height: '100%',
  expandRows: true,
  slotMinTime: '08:00',
  slotMaxTime: '17:00',
  slotDuration: '00:15',
  slotHeight: 14,
}

The calendar needs a constrained height. For height: '100%', give its parent a defined height and make framework host wrappers fill that parent as described under sizing above. Without a constrained height, slots retain their natural size.

slotHeight remains the minimum readable height in pixels. Taller containers distribute spare height across the visible time slots; shorter containers retain the minimum and scroll. Day, resource, and all-day headers keep their natural sizes. Changes to container size, header content, or visible time limits recalculate the effective slot height, including after a hidden container becomes visible.

Event placement, minimum event heights, background events, drag/selection previews, pointer-to-time mapping, scrolling, and now indicators use the same effective height. getOption('slotHeight') continues to return the configured minimum. Do not override the internal slot CSS properties to stretch rows. This option does not change month, list, or horizontal timeline sizing.

Timezone and formatting

timeZone defaults to 'local'. Use an IANA zone such as 'America/New_York', 'UTC', or a fixed offset. Invalid zones warn and fall back to the local zone; timeZone: undefined restores 'local'. date defaults to today in the calendar's zone. Explicitly set it for deterministic examples and tests.

locale accepts a locale string or preference array. Date/time format options such as eventTimeFormat, dayHeaderFormat, titleFormat, and slotLabelFormat accept Intl.DateTimeFormatOptions or a function receiving Temporal values. Button labels are configured separately with buttonText; setting a locale does not translate every UI string.

import type { CalendarOptions } from 'z-cal'

export const options: CalendarOptions = {
  timeZone: 'Europe/Paris',
  locale: 'fr-FR',
  firstDay: 1,
  eventTimeFormat: { hour: '2-digit', minute: '2-digit', hour12: false },
  buttonText: { today: "Aujourd'hui", timeGridWeek: 'Semaine', timeGridDay: 'Jour' },
}

See date semantics before choosing offset-free versus UTC event inputs.

Resources

Resource views need both the relevant plugin and resources. Associate events using resourceId or resourceIds. IDs normalize to strings. Timeline resources may include nested children and expanded flags; resourceExpand reports user changes.

import 'z-cal/polyfill'
import {
  Calendar,
  ResourceTimeGrid,
  Interaction,
  type EventInput,
  type ResourceInput,
} from 'z-cal/react'
import 'z-cal/style.css'

const plugins = [ResourceTimeGrid, Interaction]
const resources: ResourceInput[] = [
  { id: 'room-a', title: 'Room A' },
  { id: 'room-b', title: 'Room B' },
]
const events: EventInput[] = [
  {
    id: 'booking',
    resourceId: 'room-a',
    title: 'Planning',
    start: '2026-09-07T09:00',
    end: '2026-09-07T10:00',
  },
]
export function Rooms() {
  return (
    <Calendar
      plugins={plugins}
      resources={resources}
      events={events}
      date="2026-09-07"
      view="resourceTimeGridDay"
      height="650px"
      editable
    />
  )
}

resources also accepts a URL object or async/callback source. Function sources always receive the current fetch info (start, end, startStr, endStr, timeZone). With refetchResourcesOnNavigate: true, URL sources also get the range as request parameters and every source reloads on navigation; without it, a source runs once and again only on refetchResources() or when the source is replaced. getResources, getResourceById, and refetchResources are available on the instance. Use datesAboveResources to change resource time-grid header grouping.

Business hours

businessHours shades the time outside working hours and, with the constraint options, keeps edits inside it. true means Monday to Friday, 09:00 to 17:00. Pass one rule or an array of { daysOfWeek, startTime, endTime } rules (Sunday is 0; a window may pass midnight with an end such as '26:00'), or a function of the date and, in resource views, the resource. Time grids and timelines shade the closed slots; day grids and the all-day row tint whole closed days. A resource's own businessHours replaces the calendar's for its column or row.

import 'z-cal/polyfill'
import { Calendar, TimeGrid, Interaction, type BusinessHoursInput } from 'z-cal/react'
import 'z-cal/style.css'

const plugins = [TimeGrid, Interaction]
const hours: BusinessHoursInput[] = [
  { daysOfWeek: [1, 2, 3, 4, 5], startTime: '09:00', endTime: '12:00' },
  { daysOfWeek: [1, 2, 3, 4, 5], startTime: '13:00', endTime: '17:30' },
  { daysOfWeek: [6], startTime: '10:00', endTime: '14:00' },
]
export function OfficeHours() {
  return (
    <Calendar
      plugins={plugins}
      view="timeGridWeek"
      businessHours={hours}
      dragConstraint="businessHours"
      resizeConstraint="businessHours"
      selectConstraint="businessHours"
      eventOverlap={false}
      editable
      selectable
    />
  )
}

dragConstraint, resizeConstraint, and selectConstraint accept 'businessHours', inline rules (the same shapes, without shading), the id of an event the candidate must stay inside, or a predicate. eventOverlap and selectOverlap decide whether edits and selections may overlap other events. See constraints and overlap for the rules, and styling for the --cx-non-business-bg token and the nonBusiness class.