Calendar
A comprehensive React calendar component with month, week, day, and year views, plus powerful event lifecycle callbacks.
Basic Usage
Section titled “Basic Usage”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.
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} /> );}Plugins
Section titled “Plugins”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.
Basic Props
Section titled “Basic Props”| 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. |
Event Handlers
Section titled “Event Handlers”| 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. |
Examples
Section titled “Examples”With Event Handlers
Section titled “With Event Handlers”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} /> );}Internationalization
Section titled “Internationalization”The calendar supports comprehensive internationalization including locale-based formatting, custom translations, and translator functions. For detailed documentation and examples, see the Internationalization guide.
Time Format Configuration
Section titled “Time Format Configuration”Configure whether times are displayed in 12-hour or 24-hour format in week and day views.
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 Rendering
Section titled “Custom Event Rendering”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)} /> );}Drawing a cut event
Section titled “Drawing a cut event”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.
Keeping the title in view
Section titled “Keeping the title in view”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: hiddenbetween the title and the bar. A sticky element sticks to its nearest ancestor withoverflowset tohidden,scroll,autooroverlay(MDN). Under one of those it never moves. Useoverflow: clipto hide overflow instead. - Outside a grid (the “+N more” dialog, the drag overlay) the properties are
unset. There
left/topfall back toauto, so the title stays where it is.
Custom Event Bar Height
Section titled “Custom Event Bar Height”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.
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> )} /> );}Loading events
Section titled “Loading events”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.
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
timezonechanges, and when you callrefetchEvents()fromuseIlamyCalendarContext. SetlazyFetching={false}to fetch on every navigation. - A request that is replaced by a newer one is aborted through
signaland its result is ignored, even if your function never looks at the signal. - If a fetch fails, the events already on screen stay and
onEventsErroris called. The next navigation tries again. - Edits made in the calendar show immediately and are reported through
onEventAdd,onEventUpdate,onEventDeleteandonEventsChange. 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
eventsbecomes 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.
Controlling the calendar from outside
Section titled “Controlling the calendar from outside”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.
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} /> </> );}refis a regular prop in React 19, so noforwardRefis needed.ref.currentisnulluntil the calendar has mounted. Read it in event handlers and effects, not while rendering.- It always holds the latest state:
ref.current.currentDateis the new date afternextPeriod().ref.current.eventsholds the events in the visible range, the same as the hook’sevents. - To jump to a date, call
selectDate(date)(FullCalendar’sgotoDate): it callsonDateChange, so a URL or sidebar kept in sync from that callback follows.setCurrentDate(date)moves the calendar silently and never callsonDateChange. scrollToTimeacts on the view that is showing. A view you open afterwards starts wherescrollTimeputs it.
Initial View and Event Lifecycle
Section titled “Initial View and Event Lifecycle”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)} /> );}View Options
Section titled “View Options”The calendar supports multiple view modes that you can set using the initialView prop.
Month View
Section titled “Month View”Traditional monthly calendar showing the entire month at a glance.
<IlamyCalendar initialView="month" events={events} />Week View
Section titled “Week View”Weekly view showing 7 days with detailed time slots.
<IlamyCalendar initialView="week" events={events} />Day View
Section titled “Day View”Single day view with hourly rows, or finer with slotDuration={30} or {15}, for detailed scheduling.
<IlamyCalendar initialView="day" events={events} />Year View
Section titled “Year View”Annual overview showing all 12 months for long-term planning.
<IlamyCalendar initialView="year" events={events} />// 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} />Business Hours
Section titled “Business Hours”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.
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 }} /> );}Split Shifts Support
Section titled “Split Shifts Support”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.
<IlamyCalendar events={events} businessHours={[ { daysOfWeek: ['monday', 'wednesday', 'friday'], startTime: 9, // Morning shift endTime: 12, }, { daysOfWeek: ['monday', 'wednesday', 'friday'], startTime: 14, // Afternoon shift endTime: 18, } ]}/>daysOfWeek
Section titled “daysOfWeek”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.
startTime
Section titled “startTime”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
endTime
Section titled “endTime”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).
<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.
hideNonBusinessHours
Section titled “hideNonBusinessHours”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
<IlamyCalendar events={events} hideNonBusinessHours={true} businessHours={{ daysOfWeek: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'], startTime: 8, endTime: 18, }}/>Visible hours
Section titled “Visible hours”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.
<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
scrollTimebeforeslotMinTimeopens at the top. - Values run from
'00:00'to'24:00'within one day. An invalid value, or aslotMaxTimethat is not afterslotMinTime, shows the whole day and logs oneconsole.warnnaming the prop.
Hidden Days
Section titled “Hidden Days”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.
import { IlamyCalendar } from '@ilamy/calendar';
function BusinessWeekCalendar() { return ( <IlamyCalendar events={events} initialView="week" hiddenDays={['saturday', 'sunday']} /> );}<IlamyCalendar events={events} initialView="week" hiddenDays={['friday']}/>weekends={false} is shorthand for hiding Saturday and Sunday:
<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.
Moving and resizing events
Section titled “Moving and resizing events”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.
<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.
<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.
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
deltaequalsendDeltainonEventsChange. - 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
defaultTimedEventDurationis 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 oneisCellDisabledreturnstruefor.
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.
Formatting times and dates
Section titled “Formatting times and dates”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 |
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" />Custom Hour Labels
Section titled “Custom Hour Labels”Customize how hour labels are displayed in the time gutter of day and week views using the renderHour prop.
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> )} /> );}Custom Current Time Indicator
Section titled “Custom Current Time Indicator”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.
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} />}/>RenderCurrentTimeIndicatorProps
Section titled “RenderCurrentTimeIndicatorProps”The renderer receives a context object with the following properties:
currentTime- Current date/time as a Dayjs objectrangeStart- Start of the current view rangerangeEnd- End of the current view rangeprogress- Percentage (0-100) representing the current time’s position within the dayresource- The current resource (in resource calendars)view- The active view name (‘day’ or ‘week’)axis-'vertical' | 'horizontal'. Vertical mapsprogressto atopoffset (day/week vertical grids); horizontal maps it to aleftoffset (horizontal resource hour grids). The library always populates this, so consumers can treat it as defined inside their render function.
Custom Event Form
Section titled “Custom Event Form”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.
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} />} /> );}EventFormProps Properties
Section titled “EventFormProps Properties”open- Boolean indicating if the form should be visibleselectedEvent- The event being edited (null for new events)onAdd- Callback to create a new eventonUpdate- Callback to update an existing eventonDelete- Callback to delete an eventonClose- Callback to close the form (required)
Event Lifecycle Callbacks
Section titled “Event Lifecycle Callbacks”Handle event creation, updates, and deletion with powerful callback functions. These callbacks are essential for maintaining your event data and integrating with backend APIs.
Event Management Callbacks
Section titled “Event Management Callbacks”onEventAdd
Section titled “onEventAdd”Triggered when a user creates a new event through the calendar interface.
Type: (event: CalendarEvent) => void
onEventUpdate
Section titled “onEventUpdate”Called when an existing event is modified (e.g., dragged or edited).
Type: (event: CalendarEvent) => void
onEventDelete
Section titled “onEventDelete”Invoked when a user deletes an event from the calendar.
Type: (event: CalendarEvent) => void
onEventsChange
Section titled “onEventsChange”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.
<IlamyCalendar events={events} plugins={[recurrencePlugin()]} onEventsChange={async (change) => { await fetch('/api/events/changes', { method: 'POST', body: JSON.stringify(change), }) }}/>onDateChange
Section titled “onDateChange”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
Complete Integration Example
Section titled “Complete Integration Example”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> );}CalendarEvent Interface
Section titled “CalendarEvent Interface”The main event object used throughout the calendar. All events passed to the calendar should conform to this interface.
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).
Event Properties Explained
Section titled “Event Properties Explained”Required Properties
Section titled “Required Properties”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 aDayjs, aDate, or an ISO string.end- When the event ends. Pass aDayjs, aDate, or an ISO string.
Optional Properties
Section titled “Optional Properties”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.
end is exclusive
Section titled “end is exclusive”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.
Example Event Objects
Section titled “Example Event Objects”// Regular timed eventconst 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 eventconst 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'};BusinessHours Interface
Section titled “BusinessHours Interface”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).
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;}Usage Examples
Section titled “Usage Examples”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}/>CellInfo Interface
Section titled “CellInfo Interface”Information passed to the onCellClick, isCellDisabled, and getCellClassName callbacks. Uses named properties for better extensibility and clarity.
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;}Usage Example
Section titled “Usage Example”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}/>Disabling Cells
Section titled “Disabling Cells”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)}/>Styling Cells
Section titled “Styling Cells”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' : '')}/>CalendarClassesOverride Interface
Section titled “CalendarClassesOverride Interface”Custom class names to override default styling for various calendar elements. Used with the classesOverride prop to customize the appearance of calendar components.
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;}Usage Example
Section titled “Usage Example”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}/>EventFormProps Interface
Section titled “EventFormProps Interface”Props passed to custom event form components via the renderEventForm prop. Use this interface to build your own event creation and editing forms.
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;}Usage Example
Section titled “Usage Example”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} />}/>