Skip to content

Calendar

A comprehensive React calendar component with month, week, day, and year views, plus powerful event lifecycle callbacks.

The Calendar component is the main component for displaying calendar views. It supports various props for customization and event handling. Events should follow the CalendarEvent interface.

Basic Calendar
import { IlamyCalendar } from '@ilamy/calendar';
const events = [
{
id: '1',
title: 'Team Meeting',
start: new Date('2024-01-15T10:00:00'),
end: new Date('2024-01-15T11:00:00'),
description: 'Weekly team sync',
backgroundColor: '#3b82f6',
color: 'black'
},
{
id: '2',
title: 'Project Deadline',
start: new Date('2024-01-20T23:59:59'),
end: new Date('2024-01-20T23:59:59'),
allDay: true,
backgroundColor: '#ef4444',
color: 'black'
}
];
function MyCalendar() {
return (
<IlamyCalendar events={events} />
);
}

The v2 core is plugin-agnostic and ships no plugins by default. Capabilities like recurring events and the agenda view are opt-in plugins you register through the plugins prop. Each plugin is imported from its own subpath so you only ship the code you use.

import { IlamyCalendar } from '@ilamy/calendar';
import { recurrencePlugin } from '@ilamy/calendar/plugins/recurrence';
import { agendaPlugin } from '@ilamy/calendar/plugins/agenda';
<IlamyCalendar
events={events}
plugins={[recurrencePlugin(), agendaPlugin({ window: 'week' })]}
/>
Plugin Import Adds
recurrencePlugin() @ilamy/calendar/plugins/recurrence RFC 5545 recurring-event expansion, scoped edit/delete, and the rrule/recurrenceId/exdates fields on CalendarEvent. See Recurring Events.
agendaPlugin({ window }) @ilamy/calendar/plugins/agenda An agenda (upcoming-events list) view scoped to a window ('day' | 'week' | 'month', or a number of days). See Agenda.
dragToCreatePlugin({ onSelect }) @ilamy/calendar/plugins/drag-to-create Drag across empty cells to select a range and open the event form with it. See Drag to Create.

The only valid import entry points are @ilamy/calendar, @ilamy/calendar/plugins/recurrence, @ilamy/calendar/plugins/agenda, and @ilamy/calendar/plugins/drag-to-create (plus @ilamy/calendar/testing). Deep imports into the package internals are blocked in v2.

Prop Type Default Description
ref Ref<IlamyCalendarApi> - Drives the calendar from outside it: ref.current is what useIlamyCalendarContext() returns. See Controlling the calendar from outside
events CalendarEvent[] | EventsFetcher [] The events to display: an array, or a function that loads them for the visible range. See Loading events
lazyFetching boolean true With an events function: fetch only when the visible range leaves the range last fetched
loading (isLoading: boolean) => void - With an events or resources function: called with true when a fetch starts and false when none is in flight any more, for both together
onEventsError (error: unknown) => void console.error With an events function: called when a fetch fails. The events on screen stay, and the next navigation tries again
plugins IlamyPlugin[] [] Opt-in plugins that add behavior/UI. The core ships none; pass recurrencePlugin() for recurring events or agendaPlugin() for the agenda view. See Plugins below.
resources Resource[] | ResourcesFetcher [] Resources (rooms, people, equipment) to render as a resource axis. See Resource Calendar.
refetchResourcesOnNavigate boolean false With a resources function: also fetch when the visible range leaves the last fetched one. See Loading resources
resourcesSet (resources: Resource[]) => void - Called once the resource list is set and after every change
onResourcesError (error: unknown) => void console.error With a resources function: called when a fetch fails. The resources on screen stay. Navigating does not retry it unless refetchResourcesOnNavigate is set
orientation 'horizontal' | 'vertical' 'horizontal' Resource axis arrangement. Only applies when resources is set.
renderResource (resource: Resource) => ReactNode - Custom renderer for resource header cells.
weekViewGranularity 'hourly' | 'daily' 'hourly' Week-view slot granularity when resources is set.
initialView CalendarView ('month' | 'week' | 'day' | 'year', or a plugin view name) 'month' Sets the initial view when the calendar loads. CalendarView is string in v2 so plugins can register extra views.
initialDate dayjs.Dayjs | Date | string | undefined undefined (today’s date) Sets the initial date displayed when the calendar loads. When undefined, defaults to today’s date
firstDayOfWeek 'sunday' | 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' 'sunday' The first day of the week to display. It decides the week start whatever the locale (fr and de weeks start on Monday, ar on Saturday), so set it to match your locale’s convention
dayMaxEvents number 4 Maximum number of events to display in a day cell
renderEvent (event: CalendarEvent, segment: EventSegment) => ReactNode - Custom function to render individual events. segment says which slice of the event this bar is drawing
locale string 'en' Locale for date formatting (e.g., “en”, “fr”, “de”)
timezone string Whatever your local timezone is Timezone for date handling (e.g., “UTC”, “America/New_York”)
timeFormat '12-hour' | '24-hour' '12-hour' Time format for times in week and day views. '12-hour' shows ‘1:00 PM’ (and ‘1 PM’ in the hour gutter); '24-hour' shows ‘13:00’ on event bars and in the form, but only the hour (‘13’) in the gutter
slotHeaderFormat Intl.DateTimeFormatOptions - How the time-gutter labels are written, in your locale and timezone, e.g. { hour: '2-digit', minute: '2-digit', hour12: false }. When the options leave out hour12 and hourCycle, a timeFormat you passed decides; without one, the locale does. renderHour takes precedence. Format strings and functions are not supported.
eventTimeFormat Intl.DateTimeFormatOptions - How the time on an event bar is written. Setting it shows the time unless displayEventTime is false. renderEvent takes precedence.
displayEventTime boolean Shown only when eventTimeFormat is set Whether the built-in event bar shows the event’s time. All-day events never show one.
displayEventEnd boolean false Whether the time on an event bar runs to the end, e.g. 09:30 – 10:30.
dayHeaderFormat Intl.DateTimeFormatOptions Weekday over day number How a day header is written, e.g. { weekday: 'long' } gives “Wednesday”. When set, the formatted text is the whole header, with no day number added. Applies to week, day, month and resource headers.
titleFormat Intl.DateTimeFormatOptions Per view How the toolbar title is written, e.g. { month: 'long', year: 'numeric' }. The title is the view’s whole range written with Intl.DateTimeFormat#formatRange: the first to last day of the month in month view, of the year in year view.
hideNonBusinessHours boolean false Whether to hide hours outside of business hours in day and week views. Requires businessHours to be configured.
slotMinTime string '00:00' First visible time in the hour grids (day, week, and resource hour views), 'HH:mm' or 'HH:mm:ss'. See Visible hours.
slotMaxTime string '24:00' End of the visible time in the hour grids, exclusive, 'HH:mm' up to '24:00'. See Visible hours.
slotDuration 15 | 30 | 60 60 Granularity of the time grid in minutes for day, week, and resource hour views. 60 shows one row per hour with no sub-hour lines; 30 shows two; 15 shows four with dashed separators. Note: in v1.7.0 the day-view and resource-day-vertical defaults flipped from 15-minute to hourly slots; pass slotDuration={15} to restore the prior look.
snapDuration 15 | 30 the cell’s size The size, in minutes, that a dragged or resized event and a drag-to-create selection snap to. Takes effect only where it is finer than the cell and divides it: with slotDuration={60} and snapDuration={15} an event dropped halfway down an hour row lands on the half hour. Applies in the vertical time grid and in the horizontal resource timeline, whose columns are hours. Otherwise the cell’s size is used, silently. This is decided per cell: slotDuration={30} with snapDuration={30} is no snap in the vertical grid but snaps to 30 in the hourly horizontal timeline.
scrollTime string - Initial scroll position for hour-resolution views. Accepts "HH:mm" or "HH:mm:ss". Scrolls to the exact minute: "08:30" lands halfway into the 8 AM row. Reapplied on navigation, visible hour changes (not a resource added with the same hours), and whenever scrollTime changes. Clamps silently to the edge of the nearest visible row when hideNonBusinessHours hides the requested hour. Independent of businessHours. Requires a fixed calendar height to scroll.
scrollToNow boolean false Open every scrolling grid on the current moment: the current moment (to the minute) in time grids, today’s column or row in resource month and daily resource week. Applies only while now is on screen; otherwise falls back to scrollTime, or to the start. Reapplied on navigation, visible hour changes, and when the prop changes, not as the clock moves. Requires a fixed calendar height.
hiddenDays WeekDays[] [] Days of the week to hide. A hidden day gets no column or row, in the month, week and day views, with or without resources, in every orientation. Not applied to the year view or the agenda view. Hiding all seven days is ignored, with a warning in development.
weekends boolean true false hides Saturday and Sunday, the same as adding both to hiddenDays.
allDaySlot boolean true Show the all-day row in day, week and resource time grids. false removes the row and all-day events are not shown in those views. Month, year and agenda views are unaffected, and horizontal resource timelines have no all-day row to hide.
nowIndicator boolean true Show a line at the current time in time grids.
weekNumbers boolean - Unset keeps the default: the week number sits in the corner of the week and resource week headers (the number of the week’s first drawn day), and month view shows none. true also shows a number at the start of each month row, counted from the row’s first day. false hides week numbers everywhere. Resource month grids show no week numbers.
weekNumberCalculation 'local' | 'ISO' 'local' How a week is counted: the locale’s own count, ISO 8601 (week 1 holds the first Thursday; it only changes the number, set firstDayOfWeek for Monday weeks).
stickyViewHeader boolean true Whether to stick the view header to the top
viewHeaderClassName string '' Custom class name for the view header
headerComponent ReactNode null Custom header component to render above the calendar
headerClassName string undefined Custom class name applied to the calendar header (the toolbar with navigation and view controls).
hideExportButton boolean false Hide the “Export” (.ics download) button in the calendar header.
renderCurrentTimeIndicator (context: RenderCurrentTimeIndicatorProps) => ReactNode - Custom function to render the current time indicator (line).
renderHour (date: dayjs.Dayjs) => ReactNode - Custom function to render hour labels in the time gutter of day and week views. Receives a Dayjs object for the hour.
disableCellClick boolean false Disable cell click interactions
disableEventClick boolean false Disable event click interactions
disableDragAndDrop boolean false Deprecated, use editable={false}. Only sets the editable default, so an explicit eventStartEditable still enables moving
editable boolean true Whether events can be moved and resized. A per-event editable, startEditable or durationEditable overrides it.
eventStartEditable boolean editable Whether events can be moved
eventDurationEditable boolean editable Whether events can be resized by dragging an edge. Resize needs a mouse or pen: on touch-only devices events can be moved but not resized until event selection lands
eventResizableFromStart boolean false Also show a resize handle on the start edge. The end handle shows whenever resizing is allowed
eventAllow (dropInfo: EventDropInfo, draggedEvent: CalendarEvent) => boolean - Veto a candidate placement while an event is dragged or resized. dropInfo is { start, end, allDay, resourceId? }; draggedEvent is the event before the drag. Returning false shows the not-allowed cursor and a release there changes nothing
eventSpacing number 1 Spacing (in pixels) between events in calendar views. Allows customizable gaps between events for better visual clarity.
eventHeight number 24 Height (in pixels) of event bars in horizontal grid views (month, resource month, resource week horizontal). Increase to show multi-line content like title + time. Does not affect day/week views which scale with event duration.
businessHours BusinessHours | BusinessHours[] undefined Restrict calendar interactions to specified days and time ranges. Users can only create and edit events within business hours. Supports single object or array for different hours per day.
renderEventForm (props: EventFormProps) => ReactNode undefined Custom function to render the event form for creating and editing events. When provided, replaces the default event form.
classesOverride CalendarClassesOverride undefined Custom class names to override default styling for various calendar elements, including disabled cells.
translations Translations - Translations object for internationalization. If both translations and translator are provided, translator takes priority.
translator TranslatorFunction - Translator function for internationalization. Takes priority over translations object if both are provided.
Prop Type Description
onEventClick (event: CalendarEvent) => void Called when an event is clicked
onCellClick (info: CellInfo) => void Called when a date cell is clicked. Receives a CellInfo object with start, end, and optional resource (the full resource object in resource calendars).
isCellDisabled (info: CellInfo) => boolean Return true to disable a cell: blocks event-creation clicks, rejects drops onto the cell, and grays it out. Composes with businessHours via OR (a cell is disabled if business hours OR isCellDisabled disables it).
getCellClassName (info: CellInfo) => string Return extra CSS classes for a cell from custom logic (unavailability styling, on-call windows, etc.). Receives the same CellInfo as onCellClick. Unlike isCellDisabled, this is purely visual: cells stay clickable and keep accepting drops. Return '' for cells you do not want to style.
onMoreEventsClick (day: dayjs.Dayjs, events: CalendarEvent[]) => void Called when the “+N more” overflow indicator in a day cell is clicked, receiving the cell’s day and its full event list. When provided, it replaces the built-in all-events dialog. See More Events Overflow.
onViewChange (view: CalendarView) => void Called when the calendar view changes. CalendarView is string in v2, so plugin views (e.g. 'agenda') are included
onEventAdd (event: CalendarEvent) => void Called when a new event is created
onEventUpdate (event: CalendarEvent) => void Called when an existing event is updated
onEventDelete (event: CalendarEvent) => void Called when an event is deleted
onEventsChange (change: EventsChange) => void Called once per user action with every stored row it touched (added, updated, deleted), for saving a recurring edit as one unit. The per-row callbacks above still fire. See onEventsChange
onDateChange (date: dayjs.Dayjs, range: { start: dayjs.Dayjs; end: dayjs.Dayjs }) => void Called when the calendar date changes or the view changes, through the buttons, the date picker or selectDate, nextPeriod, prevPeriod, today and setView on the ref or the hook. setCurrentDate does not call it. The range contains the visible date range for the current view. When date and view change together (e.g. clicking a day in the year view), it fires once with the final date and range.
Event Handlers Example
import { IlamyCalendar } from '@ilamy/calendar';
function MyCalendar() {
const handleEventClick = (event) => {
console.log('Event clicked:', event);
// Open event details modal, navigate to event page, etc.
};
const handleDateClick = (info) => {
console.log('Date clicked:', info.start, info.end);
// Create new event, switch to day view, etc.
// info contains: { start: Dayjs, end: Dayjs, resource?: Resource, allDay?: boolean }
};
const handleViewChange = (view) => {
console.log('View changed to:', view);
// Update URL, track analytics, etc.
};
return (
<IlamyCalendar
events={events}
firstDayOfWeek="monday"
onEventClick={handleEventClick}
onCellClick={handleDateClick}
onViewChange={handleViewChange}
/>
);
}

The calendar supports comprehensive internationalization including locale-based formatting, custom translations, and translator functions. For detailed documentation and examples, see the Internationalization guide.

Configure whether times are displayed in 12-hour or 24-hour format in week and day views.

12-Hour and 24-Hour Time Formats
import { IlamyCalendar } from '@ilamy/calendar';
function MyCalendar() {
return (
<>
{/* 12-hour format (default): displays as "1:00 PM", "2:30 PM" */}
<IlamyCalendar
events={events}
initialView="week"
timeFormat="12-hour"
/>
{/* 24-hour format: bars and the form show "13:00", "14:30"; the hour gutter shows "13", "14" */}
<IlamyCalendar
events={events}
initialView="week"
timeFormat="24-hour"
/>
</>
);
}
Custom Event Renderer
import { IlamyCalendar } from '@ilamy/calendar';
function CustomCalendar() {
// `segment` says which slice of the event this bar draws; see
// "Drawing a cut event" below.
const renderEvent = (event, segment) => (
<div className="px-2 py-1 rounded bg-blue-100 text-blue-800">
<div className="font-semibold">{event.title}</div>
<div className="text-xs">{event.description}</div>
</div>
);
return (
<IlamyCalendar
events={events}
renderEvent={renderEvent}
onEventClick={(event) => alert(`Clicked: ${event.title}`)}
onCellClick={(info) => console.log('Selected date:', info.start, info.end)}
/>
);
}

A booking longer than the row it starts in is drawn as one bar per row, and renderEvent runs once for each. The second argument says which slice you have, so you can square the cut edge and round the real one:

<IlamyCalendar
events={events}
renderEvent={(event, segment) => (
<div
className={cn(
'px-2 py-1 bg-blue-100 text-blue-800',
segment.isStart ? 'rounded-l' : 'rounded-l-none',
segment.isEnd ? 'rounded-r' : 'rounded-r-none'
)}
>
{event.title}
</div>
)}
/>

isStart is true when this bar holds the event’s real start, isEnd when it holds the real end. Both are true for an event that fits in one row. The names and their meaning match FullCalendar’s event render hook, so a renderer written against that behaves the same here.

When a long event is scrolled partly out of view (a booking that started before the visible part of a resource timeline, or a shift that began above the scrolled time grid), the default event content keeps its title on the visible part of the bar. The title slides along the bar and stops at its end, never past it, and the mirror drawn while you drag the event does the same. FullCalendar’s default event content behaves the same way. The title also stays in view when a calendar taller than the window scrolls with the page, below the view header if stickyViewHeader pins it there. FullCalendar only does that with height: 'auto', where it has no scroller of its own.

A custom renderEvent owns its markup, so nothing in it sticks unless you ask. Each grid publishes, as two CSS custom properties on its scroll viewport, how far into it the reader cannot see: past its own sticky resource column and header, and past whatever hides it from outside (the top of the window, or a view header pinned there, when a calendar taller than the window scrolls with the page):

Property Value
--ilamy-sticky-left The widest sticky column (the resource column, the time or date gutter, or the “All day” label), or how far the grid is scrolled off the left of the window, whichever is further
--ilamy-sticky-top The sticky view header, or how far the page has scrolled the grid under the window’s top or a header pinned there, whichever is further; 0px when nothing covers it

They update as the page or any scrolling container around the calendar scrolls.

Offset a sticky title by them. Offsetting it by 0 would pin it under the resource column instead of beside it:

<IlamyCalendar
events={events}
renderEvent={(event) => (
<div className="h-full px-1 bg-blue-100 text-blue-800">
<span className="sticky left-(--ilamy-sticky-left) top-(--ilamy-sticky-top) inline-block max-w-full">
{event.title}
</span>
</div>
)}
/>
  • inline-block max-w-full: a title as wide as its bar has no room to slide.
  • No overflow: hidden between the title and the bar. A sticky element sticks to its nearest ancestor with overflow set to hidden, scroll, auto or overlay (MDN). Under one of those it never moves. Use overflow: clip to hide overflow instead.
  • Outside a grid (the “+N more” dialog, the drag overlay) the properties are unset. There left/top fall back to auto, so the title stays where it is.

By default, event bars in month view are 24px tall — enough for a single line of text. Use eventHeight to increase the height and show more content, such as a title and time range on separate lines. Pair with renderEvent for full control over what’s displayed.

Multi-Line Event Bars
import { IlamyCalendar } from '@ilamy/calendar';
function MyCalendar() {
return (
<IlamyCalendar
events={events}
eventHeight={48}
renderEvent={(event) => (
<div className="h-full w-full bg-blue-500 text-white px-1 rounded-md overflow-clip">
<p className="text-xs font-semibold">{event.title}</p>
<p className="text-[10px] opacity-80">
{event.start.format('h:mm A')} - {event.end.format('h:mm A')}
</p>
</div>
)}
/>
);
}

Pass a function as events and the calendar calls it with the visible range, so you load only what is on screen. In month view the range includes the leading and trailing days of the neighbouring months, and end is exclusive.

Events from an API
import { IlamyCalendar, type EventsFetcher } from '@ilamy/calendar';
import { useState } from 'react';
const fetchEvents: EventsFetcher = async ({ startStr, endStr }, { signal }) => {
const response = await fetch(`/api/events?start=${startStr}&end=${endStr}`, { signal });
if (!response.ok) throw new Error('Could not load events');
return response.json();
};
function LoadedCalendar() {
const [isLoading, setIsLoading] = useState(false);
return (
<>
{isLoading && <Spinner />}
<IlamyCalendar
events={fetchEvents}
loading={setIsLoading}
onEventsError={(error) => console.error(error)}
/>
</>
);
}
  • The calendar fetches again when you navigate outside the range it last fetched, when timezone changes, and when you call refetchEvents() from useIlamyCalendarContext. Set lazyFetching={false} to fetch on every navigation.
  • A request that is replaced by a newer one is aborted through signal and its result is ignored, even if your function never looks at the signal.
  • If a fetch fails, the events already on screen stay and onEventsError is called. The next navigation tries again.
  • Edits made in the calendar show immediately and are reported through onEventAdd, onEventUpdate, onEventDelete and onEventsChange. The next fetch replaces what the calendar holds with what your function returns, so save changes to your server before it runs.
  • Passing a new function on each render does not refetch. Call refetchEvents() when your query changes (a filter, a different calendar).
  • If events becomes an array while a fetch is in flight, that fetch’s result is dropped and never lands on the array. Switching back to a function fetches again.
  • With the recurrence plugin, return the series rows (with their overrides and exdates) whose recurrence intersects the range, not only rows whose first occurrence overlaps it. The calendar expands a series from its row, so a series your function leaves out disappears, including its later occurrences.
  • Several event sources and JSON feed URLs are not supported.

Pass a ref to drive the calendar from your own toolbar, a sidebar or a “jump to” button. ref.current is the same object useIlamyCalendarContext returns, so the names are the same.

A toolbar outside the calendar
import { IlamyCalendar, type IlamyCalendarApi } from '@ilamy/calendar';
import { useRef } from 'react';
function Planner() {
const calendarRef = useRef<IlamyCalendarApi>(null);
return (
<>
<button onClick={() => calendarRef.current?.prevPeriod()}>Back</button>
<button onClick={() => calendarRef.current?.today()}>Today</button>
<button onClick={() => calendarRef.current?.nextPeriod()}>Forward</button>
<button onClick={() => calendarRef.current?.setView('week')}>Week</button>
<button onClick={() => calendarRef.current?.scrollToTime('13:30')}>
Jump to 1:30 pm
</button>
<IlamyCalendar events={events} ref={calendarRef} />
</>
);
}
  • ref is a regular prop in React 19, so no forwardRef is needed.
  • ref.current is null until the calendar has mounted. Read it in event handlers and effects, not while rendering.
  • It always holds the latest state: ref.current.currentDate is the new date after nextPeriod(). ref.current.events holds the events in the visible range, the same as the hook’s events.
  • To jump to a date, call selectDate(date) (FullCalendar’s gotoDate): it calls onDateChange, so a URL or sidebar kept in sync from that callback follows. setCurrentDate(date) moves the calendar silently and never calls onDateChange.
  • scrollToTime acts on the view that is showing. A view you open afterwards starts where scrollTime puts it.
Advanced Calendar Configuration
import { IlamyCalendar, type CalendarEvent, type Dayjs } from '@ilamy/calendar';
import { useState } from 'react';
function AdvancedCalendar() {
const [events, setEvents] = useState<CalendarEvent[]>([]);
const handleEventAdd = (event: CalendarEvent) => {
console.log('New event added:', event);
setEvents(prev => [...prev, event]);
// Save to your backend API
};
const handleEventUpdate = (updatedEvent: CalendarEvent) => {
console.log('Event updated:', updatedEvent);
setEvents(prev =>
prev.map(event =>
event.id === updatedEvent.id ? updatedEvent : event
)
);
// Update in your backend API
};
const handleEventDelete = (deletedEvent: CalendarEvent) => {
console.log('Event deleted:', deletedEvent);
setEvents(prev =>
prev.filter(event => event.id !== deletedEvent.id)
);
// Delete from your backend API
};
const handleDateChange = (date: Dayjs, range: { start: Dayjs; end: Dayjs }) => {
console.log('Date changed to:', date.format('YYYY-MM-DD'));
console.log('Visible range:', range.start.format('YYYY-MM-DD'), 'to', range.end.format('YYYY-MM-DD'));
// Load events for the visible date range
// fetchEventsForRange(range.start, range.end);
};
return (
<IlamyCalendar
events={events}
initialView="week" // Start with week view
onEventAdd={handleEventAdd}
onEventUpdate={handleEventUpdate}
onEventDelete={handleEventDelete}
onDateChange={handleDateChange}
onEventClick={(event) => console.log('Event clicked:', event)}
onCellClick={(info) => console.log('Cell clicked:', info.start, info.end)}
/>
);
}

The calendar supports multiple view modes that you can set using the initialView prop.

Traditional monthly calendar showing the entire month at a glance.

<IlamyCalendar initialView="month" events={events} />

Weekly view showing 7 days with detailed time slots.

<IlamyCalendar initialView="week" events={events} />

Single day view with hourly rows, or finer with slotDuration={30} or {15}, for detailed scheduling.

<IlamyCalendar initialView="day" events={events} />

Annual overview showing all 12 months for long-term planning.

<IlamyCalendar initialView="year" events={events} />
Setting Different Initial Views
// Month view (default)
<IlamyCalendar initialView="month" events={events} />
// Week view for detailed scheduling
<IlamyCalendar initialView="week" events={events} />
// Day view for hourly planning
<IlamyCalendar initialView="day" events={events} />
// Year view for long-term overview
<IlamyCalendar initialView="year" events={events} />

Restrict calendar interactions to specific days and time ranges. When configured, users can only create and edit events within business hours. This is particularly useful for appointment booking, scheduling applications, and enforcing availability constraints.

Configuring Business Hours
import { IlamyCalendar } from '@ilamy/calendar';
function BusinessCalendar() {
const events = [/* your events */];
return (
<IlamyCalendar
events={events}
businessHours={{
daysOfWeek: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
startTime: 9, // 9 AM
endTime: 17, // 5 PM
}}
/>
);
}

You can define multiple business hour ranges for the same day (split shifts) by providing an array of BusinessHours objects. This allows for scheduled breaks or multiple working sessions. Sub-hour 'HH:mm' boundaries work here too (e.g. a '09:15'–'12:15' morning shift).

Note that each range is checked independently — adjacent ranges do not merge. A time slot that straddles the seam between two touching ranges (e.g. an hour slot over 12:00–13:00 with ranges ending and starting at '12:30') counts as non-business. If the combined window is really one continuous range, define it as one.

Configuring Split Shifts
<IlamyCalendar
events={events}
businessHours={[
{
daysOfWeek: ['monday', 'wednesday', 'friday'],
startTime: 9, // Morning shift
endTime: 12,
},
{
daysOfWeek: ['monday', 'wednesday', 'friday'],
startTime: 14, // Afternoon shift
endTime: 18,
}
]}
/>

Array of day names that are considered business days. Users can only interact with the calendar on these days.

Type: string[] Valid values: 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' Default: every day

Leaving daysOfWeek out applies the hours to all seven days, weekends included, whether you pass one object or an array. List the days explicitly to restrict them, for example ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'] for weekdays only. This differs from FullCalendar, which defaults to Monday to Friday.

Time when business hours begin (inclusive). Events cannot be created or modified before this time. Use a whole number for on-the-hour boundaries or an 'HH:mm' string for sub-hour boundaries such as '09:15'. Fractional numbers are not supported and round to the nearest whole hour.

Type: number (0-24) | string ('HH:mm') Default: 9

Time when business hours end (exclusive). Events cannot be created or modified at or after this time. Accepts the same formats as startTime. '24:00' is a valid end-of-day boundary.

Type: number (0-24) | string ('HH:mm') Default: 17

Malformed or out-of-range strings ('9am', '9:99', '25:00') are ignored and fall back to the defaults (9 for startTime, 17 for endTime).

Sub-Hour Business Hours
<IlamyCalendar
events={events}
slotDuration={15}
businessHours={{
daysOfWeek: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
startTime: '09:15',
endTime: '17:15',
}}
/>

A time slot is interactive only when it fits entirely inside business hours. On grids coarser than the boundary — for example default one-hour cells with a '09:15' start — the partially-covered slot counts as non-business, so the calendar never allows a booking that violates the boundary. Set slotDuration to match your finest boundary (as above) for exact behavior.

When enabled, the calendar will only display the hours defined in the businessHours configuration, hiding the rest of the day. This is supported in Day and Week views (both vertical and horizontal orientations). With sub-hour boundaries, the hour rows hosting a boundary stay visible (a '09:15' start keeps the 9:00 row on screen); slots outside the boundary within those rows are simply disabled.

Type: boolean Default: false

Hiding Non-Business Hours
<IlamyCalendar
events={events}
hideNonBusinessHours={true}
businessHours={{
daysOfWeek: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
startTime: 8,
endTime: 18,
}}
/>

slotMinTime and slotMaxTime choose the clock range the hour grids show: day, week, and the resource hour views in both orientations. Month, year, agenda and weekViewGranularity: 'daily' are unaffected. slotMaxTime is exclusive, so '18:00' ends after the 5 PM row.

Showing 8 AM to 6 PM
<IlamyCalendar events={events} slotMinTime="08:00" slotMaxTime="18:00" />
  • An event outside the range is not drawn. One that crosses an edge is clipped to it; the event itself keeps its real start and end.
  • A bound inside an hour ('08:30') shows that whole hour row.
  • Combined with hideNonBusinessHours, the grid shows the overlap of the two ranges.
  • The current-time line is drawn only while now is inside the range, and a scrollTime before slotMinTime opens at the top.
  • Values run from '00:00' to '24:00' within one day. An invalid value, or a slotMaxTime that is not after slotMinTime, shows the whole day and logs one console.warn naming the prop.

Hide specific weekdays. A hidden day gets no column or row, and the remaining days expand to fill the available space. It applies to the month, week and day views, with or without resources and in every orientation, but not to the year view or the agenda view.

In the day view, previous, next and today skip hidden days, and the view opens on the next visible day when it is given a hidden one. In the week view the title names only the days that are drawn.

Hiding Weekends
import { IlamyCalendar } from '@ilamy/calendar';
function BusinessWeekCalendar() {
return (
<IlamyCalendar
events={events}
initialView="week"
hiddenDays={['saturday', 'sunday']}
/>
);
}
Hiding a Single Day
<IlamyCalendar
events={events}
initialView="week"
hiddenDays={['friday']}
/>

weekends={false} is shorthand for hiding Saturday and Sunday:

Hiding Weekends with weekends
<IlamyCalendar events={events} initialView="month" weekends={false} />

An event that runs across a hidden day is drawn as one bar over the days that are drawn. One that starts or ends on a hidden day is cut at the nearest day that is drawn (isStart or isEnd in the renderEvent segment is false there), and one that lies wholly on hidden days is not drawn.

Events can be dragged to another time, day or resource, and resized by dragging an edge. Both are on by default. editable turns them off for the whole calendar, and a flag on one event overrides it.

A read-only calendar with one event you can move
<IlamyCalendar
editable={false}
events={[
{ id: 'locked', title: 'Booked', start: '2025-03-12T09:00:00.000Z', end: '2025-03-12T10:00:00.000Z' },
{ id: 'free', title: 'Tentative', start: '2025-03-12T11:00:00.000Z', end: '2025-03-12T12:00:00.000Z', editable: true },
]}
/>
Where Flag Controls
Calendar editable Moving and resizing. disableDragAndDrop is a deprecated alias of editable={false}
Calendar eventStartEditable, eventDurationEditable Moving, and resizing, on their own. Each beats editable
Event editable, startEditable, durationEditable The same three, for one event. An event’s flag beats the calendar’s, and within one level the specific flag beats editable

Drag an event’s end edge to change when it ends. Pass eventResizableFromStart to show a handle on the start edge as well. Time grids snap the edge to the slot, day grids to whole days. A resize stays in its grid and keeps the event’s resource. Resizing needs a mouse or a pen; on a touch screen events can be moved but not resized.

snapDuration (15 or 30) lets a drag, a resize and a drag-to-create selection snap finer than the slot. It applies in the vertical time grids and in the horizontal resource timeline. With slotDuration={60} and snapDuration={15} an event dropped halfway down an hour row lands on the half hour.

Resizable from both edges, quarter-hour snapping
<IlamyCalendar
events={events}
initialView="week"
slotDuration={60}
snapDuration={15}
eventResizableFromStart
/>

eventAllow vetoes a placement while an event is dragged or resized, and again when it is dropped. dropInfo is { start, end, allDay, resourceId? } and draggedEvent is the event before the drag. A refused placement draws no preview and shows the not-allowed cursor, and releasing there changes nothing. It does not apply to the event form or to drag-to-create.

No placements in the past, and none on a closed room
import { dayjs } from '@ilamy/calendar';
<IlamyCalendar
events={events}
resources={rooms}
eventAllow={(dropInfo) =>
!dropInfo.start.isBefore(dayjs(), 'day') && dropInfo.resourceId !== 'closed-room'
}
/>

What a drop does:

  • A move keeps the event’s length on the clock. An event moved across a daylight-saving change does not grow or shrink by an hour, and delta equals endDelta in onEventsChange.
  • An all-day event dropped on a time slot becomes a timed event that starts at that slot and lasts one hour, wherever on the bar you grabbed it. The hour is fixed, as FullCalendar’s defaultTimedEventDuration is by default.
  • A timed event dropped on the all-day row becomes all-day and covers that one day, ending at the next midnight.
  • A multi-day all-day event keeps its whole days when it moves.
  • Dropping an event where it already is changes nothing. No callback fires and the recurring scope dialog does not open.
  • A disabled cell refuses the drop: a cell outside businessHours, or one isCellDisabled returns true for.

An event bar can be focused with the keyboard, and Enter or Space opens it, the same as a click (onEventClick runs, and disableEventClick is honoured). A key pressed inside your own renderEvent content is left to that content. Moving and resizing need a pointer for now.

Six props change how the calendar writes times and dates. Each takes Intl.DateTimeFormat options and uses the calendar’s locale and timezone. Leave one unset and that text is written as it always was.

Prop Formats
slotHeaderFormat The hour labels in the time gutter
eventTimeFormat The time on an event bar. Setting it shows the time
displayEventTime / displayEventEnd Whether the bar shows a time, and whether it runs to the end (9:30 AM – 10:30 AM)
dayHeaderFormat The day headers. The formatted text is the whole header, so { weekday: 'long' } gives “Wednesday” with no day number
titleFormat The title in the header. A week or a custom range is written with formatRange; month view formats its first to last day, year view its whole year
French week view with 24-hour labels and end times
import 'dayjs/locale/fr';
<IlamyCalendar
events={events}
initialView="week"
locale="fr"
slotHeaderFormat={{ hour: '2-digit', minute: '2-digit', hour12: false }}
eventTimeFormat={{ hour: '2-digit', minute: '2-digit', hour12: false }}
displayEventEnd
dayHeaderFormat={{ weekday: 'short', day: 'numeric' }}
titleFormat={{ month: 'long', year: 'numeric' }}
/>

Only the options object is supported: no format strings or functions. Use renderHour or renderEvent for anything else.

weekNumbers and weekNumberCalculation control week numbers. By default the week and resource-week headers show one and the month view shows none; weekNumbers adds a number to each month row and weekNumbers={false} removes them all. A week is numbered by its first drawn day, so the header, the month row and a hiddenDays week agree. weekNumberCalculation="ISO" counts ISO 8601 weeks; it does not make weeks start on Monday, firstDayOfWeek does.

<IlamyCalendar events={events} weekNumbers weekNumberCalculation="ISO" firstDayOfWeek="monday" />

Customize how hour labels are displayed in the time gutter of day and week views using the renderHour prop.

Custom Hour Labels
import { IlamyCalendar } from '@ilamy/calendar';
function MyCalendar() {
return (
<IlamyCalendar
events={events}
initialView="week"
renderHour={(date) => (
<div className="flex flex-col items-center">
<span className="text-sm font-medium">{date.format('h A')}</span>
<span className="text-xs text-muted-foreground">{date.format('HH:00')}</span>
</div>
)}
/>
);
}

You can fully customize the “current time” line that appears in day and week views using the renderCurrentTimeIndicator prop.

Branch on axis: horizontal resource hour grids (resource day/week horizontal) pass axis === 'horizontal' and expect a vertical line positioned with left; vertical day/week grids pass axis === 'vertical' and expect a horizontal line positioned with top. Ignoring axis draws a horizontal line in every view, which renders incorrectly in horizontal resource grids.

Resource-Aware Custom Current Time Indicator
import type { RenderCurrentTimeIndicatorProps } from '@ilamy/calendar';
const CustomIndicator = ({ currentTime, progress, resource, axis }: RenderCurrentTimeIndicatorProps) => {
// The indicator renders once per resource row, so show the badge only for the
// first resource to avoid repeating identical time badges. Non-resource
// calendars have no `resource`, so the badge still shows there.
const showBadge = !resource || resource.id === 'primary';
if (axis === 'horizontal') {
// Vertical line for horizontal resource hour grids — position with `left`
return (
<div
className="absolute top-0 bottom-0 z-50 pointer-events-none w-0.5 flex flex-col"
style={{ left: `${progress}%` }}
>
{showBadge && (
<div className="absolute top-0 left-1/2 -translate-x-1/2 bg-red-500 text-white text-xs px-1.5 py-0.5 rounded-b-md">
{currentTime.format('h:mm A')}
</div>
)}
<div className="flex-1 bg-red-500" />
</div>
);
}
// Horizontal line for vertical day/week grids — position with `top`
return (
<div
className="absolute left-0 right-0 z-50 pointer-events-none h-0.5 flex"
style={{ top: `${progress}%` }}
>
{showBadge && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 bg-red-500 text-white text-xs px-1.5 py-0.5 rounded-r-md">
{currentTime.format('h:mm A')}
</div>
)}
<div className="flex-1 bg-red-500" />
</div>
);
};
<IlamyCalendar
renderCurrentTimeIndicator={(props) => <CustomIndicator {...props} />}
/>

The renderer receives a context object with the following properties:

  • currentTime - Current date/time as a Dayjs object
  • rangeStart - Start of the current view range
  • rangeEnd - End of the current view range
  • progress - Percentage (0-100) representing the current time’s position within the day
  • resource - The current resource (in resource calendars)
  • view - The active view name (‘day’ or ‘week’)
  • axis - 'vertical' | 'horizontal'. Vertical maps progress to a top offset (day/week vertical grids); horizontal maps it to a left offset (horizontal resource hour grids). The library always populates this, so consumers can treat it as defined inside their render function.

Replace the default event form with your own custom implementation using the renderEventForm prop. This gives you full control over the event creation and editing experience.

Custom Event Form Implementation
import { IlamyCalendar, dayjs, type EventFormProps, type CalendarEvent } from '@ilamy/calendar';
const CustomEventForm = ({
open,
selectedEvent,
onAdd,
onUpdate,
onDelete,
onClose,
}: EventFormProps) => {
if (!open) return null;
const handleSubmit = (formData: FormData) => {
const eventData: CalendarEvent = {
id: selectedEvent?.id || `event-${Date.now()}`,
title: formData.get('title') as string,
start: selectedEvent?.start || dayjs(),
end: selectedEvent?.end || dayjs(),
};
if (selectedEvent?.id) {
onUpdate?.(eventData);
} else {
onAdd?.(eventData);
}
onClose();
};
return (
<dialog open={open} className="modal">
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit(new FormData(e.currentTarget));
}}>
<input
name="title"
defaultValue={selectedEvent?.title}
placeholder="Event title"
/>
<div className="actions">
{selectedEvent?.id && (
<button type="button" onClick={() => {
onDelete?.(selectedEvent);
onClose();
}}>Delete</button>
)}
<button type="button" onClick={onClose}>Cancel</button>
<button type="submit">
{selectedEvent?.id ? 'Update' : 'Create'}
</button>
</div>
</form>
</dialog>
);
};
function MyCalendar() {
return (
<IlamyCalendar
events={events}
renderEventForm={(props) => <CustomEventForm {...props} />}
/>
);
}
  • open - Boolean indicating if the form should be visible
  • selectedEvent - The event being edited (null for new events)
  • onAdd - Callback to create a new event
  • onUpdate - Callback to update an existing event
  • onDelete - Callback to delete an event
  • onClose - Callback to close the form (required)

Handle event creation, updates, and deletion with powerful callback functions. These callbacks are essential for maintaining your event data and integrating with backend APIs.

Triggered when a user creates a new event through the calendar interface.

Type: (event: CalendarEvent) => void

Called when an existing event is modified (e.g., dragged or edited).

Type: (event: CalendarEvent) => void

Invoked when a user deletes an event from the calendar.

Type: (event: CalendarEvent) => void

Called once per user action, with every stored row the action touched. The three callbacks above report those rows one at a time, and a recurring edit touches several: “edit all events” updates the series and deletes the occurrences that were moved, and “edit this event” updates the series and adds an override. onEventsChange reports them together, so a backend can save the action in one transaction, or refuse it as a whole. FullCalendar reports an action once the same way (eventChange with relatedEvents).

The per-row callbacks still fire. Use one or the other.

Type: (change: EventsChange) => void

interface EventsChange {
action: 'add' | 'update' | 'delete' // what the user did
event: CalendarEvent // the event the user acted on, as it was shown before the change
scope?: unknown // the scope chosen in the plugin's dialog, if any
added: CalendarEvent[] // the rows onEventAdd receives
updated: CalendarEvent[] // the rows onEventUpdate receives
deleted: CalendarEvent[] // the rows onEventDelete receives
delta?: { days: number; milliseconds: number } // 'update' only: how far the start moved
endDelta?: { days: number; milliseconds: number } // 'update' only: how far the end moved
revert: () => void // undo the whole action in the calendar's own store
}

delta says how far the event’s start moved: whole calendar days plus the change in the time of day on the wall clock (zero when only the title changed). It is undefined for an add and a delete. A timed event dropped on an all-day row is measured from the start of its own day. endDelta says how far the end moved, the same way: dragging an event’s end edge sets only endDelta, dragging the start edge sets only delta, and moving an event sets both to the same amount (a timed event moved onto the all-day row measures delta from the start of its day, so the two can differ). A move keeps the event’s length on the clock, so across a daylight-saving change delta and endDelta are equal. Dropping an event where it already is reports nothing. There is no separate resize callback: a resize is one onEventsChange action with its endDelta. event is the event as it was before the change; the new rows are in updated.

revert() undoes the action in the calendar’s own store: it removes added, restores the previous version of every row in updated and puts deleted back, and it fires no callbacks. If a later action edited one of those rows, that edit is overwritten; rows other actions added or changed are kept. Calling it a second time does nothing. Call it when your server rejects a save. If you control events, restore your own state instead, because a new events prop replaces the calendar’s rows.

With the recurrence plugin, scope is a RecurrenceEditScope ('this' | 'following' | 'all'), exported from @ilamy/calendar/plugins/recurrence. The 'all' scope keeps the series on its weekday, so dragging an occurrence to another day reports only the change in time of day.

Saving each action as one request
<IlamyCalendar
events={events}
plugins={[recurrencePlugin()]}
onEventsChange={async (change) => {
await fetch('/api/events/changes', {
method: 'POST',
body: JSON.stringify(change),
})
}}
/>

Fired when the user navigates to a different date or time period, or when the view changes, and by selectDate, nextPeriod, prevPeriod, today and setView called from the ref or the hook. It is not fired by setCurrentDate, which moves the calendar silently. The second argument provides the visible date range for the current view, enabling efficient data fetching.

Type: (date: dayjs.Dayjs, range: { start: dayjs.Dayjs; end: dayjs.Dayjs }) => void

Full Event Lifecycle Management
import { IlamyCalendar, type CalendarEvent, type Dayjs } from '@ilamy/calendar';
import { useState } from 'react';
function CalendarWithAPI() {
const [events, setEvents] = useState<CalendarEvent[]>([]);
const [loading, setLoading] = useState(false);
// Handle new event creation
const handleEventAdd = async (event: CalendarEvent) => {
setLoading(true);
try {
// Save to backend
const response = await fetch('/api/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
});
if (response.ok) {
const savedEvent = await response.json();
setEvents(prev => [...prev, savedEvent]);
console.log('Event created successfully:', savedEvent);
}
} catch (error) {
console.error('Failed to create event:', error);
} finally {
setLoading(false);
}
};
// Handle event updates
const handleEventUpdate = async (updatedEvent: CalendarEvent) => {
setLoading(true);
try {
const response = await fetch('/api/events/' + updatedEvent.id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedEvent)
});
if (response.ok) {
setEvents(prev =>
prev.map(event =>
event.id === updatedEvent.id ? updatedEvent : event
)
);
console.log('Event updated successfully:', updatedEvent);
}
} catch (error) {
console.error('Failed to update event:', error);
} finally {
setLoading(false);
}
};
// Handle event deletion
const handleEventDelete = async (deletedEvent: CalendarEvent) => {
setLoading(true);
try {
const response = await fetch('/api/events/' + deletedEvent.id, {
method: 'DELETE'
});
if (response.ok) {
setEvents(prev =>
prev.filter(event => event.id !== deletedEvent.id)
);
console.log('Event deleted successfully:', deletedEvent);
}
} catch (error) {
console.error('Failed to delete event:', error);
} finally {
setLoading(false);
}
};
// Handle date navigation and view changes
const handleDateChange = async (date: Dayjs, range: { start: Dayjs; end: Dayjs }) => {
console.log('Navigating to:', date.format('YYYY-MM-DD'));
// Load events for the visible date range
try {
const response = await fetch(
'/api/events?start=' + range.start.toISOString() +
'&end=' + range.end.toISOString()
);
if (response.ok) {
const monthEvents = await response.json();
setEvents(monthEvents);
}
} catch (error) {
console.error('Failed to load events for date:', error);
}
};
return (
<div className="relative">
{loading && (
<div className="absolute inset-0 bg-white/50 flex items-center justify-center z-10">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
)}
<IlamyCalendar
events={events}
initialView="month"
onEventAdd={handleEventAdd}
onEventUpdate={handleEventUpdate}
onEventDelete={handleEventDelete}
onDateChange={handleDateChange}
onEventClick={(event) => {
// Navigate to event details or open modal
console.log('Event clicked:', event);
}}
/>
</div>
);
}

The main event object used throughout the calendar. All events passed to the calendar should conform to this interface.

CalendarEvent (events prop input)
interface CalendarEvent {
id: string | number; // Unique identifier for the event
title: string; // Display title of the event
start: Dayjs | Date | string; // Start (inclusive) — Dayjs, Date, or ISO string
end: Dayjs | Date | string; // End (EXCLUSIVE) — Dayjs, Date, or ISO string
description?: string; // Optional description text
location?: string; // Optional location
uid?: string; // Optional iCalendar UID (used to group recurring series)
color?: string; // Optional text color (hex, rgb, named colors, class names)
backgroundColor?: string; // Optional background color (hex, rgb, named colors, class names)
allDay?: boolean; // Whether this is an all-day event
resourceId?: string | number; // Single resource assignment (for resource calendars)
resourceIds?: (string | number)[]; // Multiple resource assignment (for cross-resource events)
data?: Record<string, unknown>; // Optional custom data (narrow or cast when reading)
editable?: boolean; // Whether this event can be moved and resized. Beats the calendar's `editable`
startEditable?: boolean; // Whether this event can be moved. Defaults to `editable`
durationEditable?: boolean; // Whether this event can be resized. Defaults to `editable`
}

The events prop accepts start / end as a Dayjs, a Date, or an ISO string; the calendar normalizes them to Dayjs internally, so reads from useIlamyCalendarContext() always return Dayjs. With the recurrence plugin registered, rrule, recurrenceId, and exdates are also available on this type (see Recurring Events).

  • id - Must be unique across all events. Used for drag-and-drop and event updates.
  • title - The text displayed on the event in the calendar.
  • start - When the event begins. Pass a Dayjs, a Date, or an ISO string.
  • end - When the event ends. Pass a Dayjs, a Date, or an ISO string.
  • description - Additional text shown in tooltips or custom event renderers.
  • color - Background color for the event. Supports hex (#ff0000), rgb(255,0,0), or named colors (red).
  • allDay - When true, the event spans whole days and ignores time components.
  • editable, startEditable, durationEditable - Whether this event can be moved or resized, overriding the calendar’s flags. See Moving and resizing events.

An event runs up to, but not including, its end, matching RFC 5545 (“the DTEND property … specifies the non-inclusive end of the event”) and the Google Calendar API, where end is “The (exclusive) end time of the event”.

So an event ending at midnight stops as that day begins:

Event Days it covers
2024-01-01T00:00 to 2024-01-02T00:00 Jan 1
2024-01-01T00:00 to 2024-01-02T09:00 Jan 1 and Jan 2
All-day Jan 1 through Jan 3 start Jan 1, end Jan 4

The built-in event form shows you the last day an all-day event covers, and stores the exclusive value for you.

Various Event Types
// Regular timed event
const meeting = {
id: 'meeting-1',
title: 'Team Standup',
start: new Date('2024-01-15T09:00:00'),
end: new Date('2024-01-15T09:30:00'),
description: 'Daily team sync meeting',
color: '#3b82f6'
};
// All-day event covering Jan 1. `end` is exclusive, so it is Jan 2.
const holiday = {
id: 'holiday-1',
title: 'National Holiday',
start: new Date('2024-01-01'),
end: new Date('2024-01-02'),
allDay: true,
color: '#ef4444'
};
// Multi-day event
const conference = {
id: 'conf-1',
title: 'Tech Conference',
start: new Date('2024-01-20T08:00:00'),
end: new Date('2024-01-22T18:00:00'),
description: 'Annual technology conference',
color: '#10b981'
};

Configuration object for restricting calendar interactions to specific days and time ranges. Used with the businessHours prop. Supports both single object (same hours for all days) and array format (different hours per day).

BusinessHours Type Definition
interface BusinessHours {
// Array of day names considered business days (default: every day)
daysOfWeek?: ('monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday')[];
// When business hours start (inclusive): an hour 0-24, or 'HH:mm'. Default 9
startTime?: number | string;
// When business hours end (exclusive): an hour 0-24, or 'HH:mm'. Default 17
endTime?: number | string;
}

Standard business hours (same for all days):

import type { BusinessHours } from '@ilamy/calendar';
const businessHours: BusinessHours = {
daysOfWeek: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
startTime: 9,
endTime: 17,
};
<IlamyCalendar
events={events}
businessHours={businessHours}
/>

Different hours per day or Split Shifts (array format):

import type { BusinessHours } from '@ilamy/calendar';
const businessHours: BusinessHours[] = [
// Regular shift for some days
{
daysOfWeek: ['tuesday', 'thursday'],
startTime: 10,
endTime: 18,
},
// Split shift (Morning)
{
daysOfWeek: ['monday', 'wednesday'],
startTime: 9,
endTime: 12,
},
// Split shift (Afternoon)
{
daysOfWeek: ['monday', 'wednesday'],
startTime: 14,
endTime: 18,
},
// Weekend hours
{
daysOfWeek: ['saturday'],
startTime: 11,
endTime: 15,
},
];
<IlamyCalendar
events={events}
businessHours={businessHours}
/>

Information passed to the onCellClick, isCellDisabled, and getCellClassName callbacks. Uses named properties for better extensibility and clarity.

CellInfo Type Definition
interface CellInfo {
// Start date/time of the clicked cell
start: Dayjs;
// End date/time of the clicked cell (exclusive: the next boundary)
end: Dayjs;
// Full resource object when clicking a resource calendar cell;
// undefined in a regular calendar (optional)
resource?: Resource;
// Whether the clicked cell is an all-day cell (optional). True for the
// all-day row of the week and day views; false for a month cell and for
// the time grid
allDay?: boolean;
}

Handle cell clicks with the CellInfo object:

import { useIlamyCalendarContext, type CellInfo } from '@ilamy/calendar';
// Inside a component rendered by the calendar (see useIlamyCalendarContext)
const { openEventForm } = useIlamyCalendarContext();
const handleCellClick = (info: CellInfo) => {
const { start, end, resource, allDay } = info;
console.log('Cell clicked from', start.format(), 'to', end.format());
if (allDay) {
console.log('Clicked in all-day row');
}
// For resource calendars, the full resource object is available
if (resource) {
console.log('On resource:', resource.id);
}
// Create a new event — assign it to the cell's resource via resourceId
openEventForm({ start, end, resourceId: resource?.id, allDay });
};
<IlamyCalendar
events={events}
onCellClick={handleCellClick}
/>

Use isCellDisabled to block interactions on specific cells (holidays, days off, past dates, on-call windows, per-resource availability). Returning true blocks event-creation clicks, rejects drag-and-drop onto the cell, and grays it out. It composes with businessHours via OR.

import type { Dayjs } from '@ilamy/calendar';
const isHoliday = (date: Dayjs) =>
holidays.some((h) => date.isSame(h, 'day'));
<IlamyCalendar
events={events}
isCellDisabled={(info) => isHoliday(info.start)}
/>

Use getCellClassName to add CSS classes to specific cells based on custom logic (unavailability styling, on-call windows, highlighting). Unlike isCellDisabled, this is purely visual: cells stay clickable and keep accepting drops. Return an empty string for cells you do not want to style.

import type { Dayjs } from '@ilamy/calendar';
const isWeekend = (date: Dayjs) =>
date.day() === 0 || date.day() === 6;
<IlamyCalendar
events={events}
getCellClassName={(info) => (isWeekend(info.start) ? 'bg-amber-50' : '')}
/>

Custom class names to override default styling for various calendar elements. Used with the classesOverride prop to customize the appearance of calendar components.

CalendarClassesOverride Type Definition
interface CalendarClassesOverride {
// Custom class name for disabled cells
// Applied to: cells outside business hours/days, and days from other months in month view
disabledCell?: string;
}

Customize the styling of disabled cells:

import type { CalendarClassesOverride } from '@ilamy/calendar';
const classesOverride: CalendarClassesOverride = {
disabledCell: 'opacity-30 bg-gray-100 cursor-not-allowed',
};
<IlamyCalendar
events={events}
businessHours={{
daysOfWeek: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
startTime: 9,
endTime: 17,
}}
classesOverride={classesOverride}
/>

Props passed to custom event form components via the renderEventForm prop. Use this interface to build your own event creation and editing forms.

EventFormProps Type Definition
interface EventFormProps {
// Whether the form should be displayed
open?: boolean;
// The event being edited (null/undefined for new events)
selectedEvent?: CalendarEvent | null;
// Callback to create a new event
onAdd?: (event: CalendarEvent) => void;
// Callback to update an existing event
onUpdate?: (event: CalendarEvent) => void;
// Callback to delete an event
onDelete?: (event: CalendarEvent) => void;
// Callback to close the form (required)
onClose: () => void;
}

Implementing a custom event form with EventFormProps:

import type { EventFormProps } from '@ilamy/calendar';
const MyEventForm = (props: EventFormProps) => {
const { open, selectedEvent, onAdd, onUpdate, onDelete, onClose } = props;
if (!open) return null;
return (
<div className="modal">
<h2>{selectedEvent ? 'Edit Event' : 'New Event'}</h2>
{/* Your custom form UI */}
<button onClick={onClose}>Close</button>
</div>
);
};
// Usage
<IlamyCalendar
events={events}
renderEventForm={(props) => <MyEventForm {...props} />}
/>