Skip to main content

API Reference

<MhCalendar> (<mh-calendar> as a plain custom element) takes two top-level props:

PropTypeDescription
configIMHCalendarFullOptionsView type, behavior toggles, styling, business hours: everything except the events themselves. Documented field-by-field below.
eventsIMHCalendarEvent[]The events to render. See events.

config is a single flat object, merged internally from three layers (base behavior, multi-view/grid options, week/resource-specific options): you never nest anything, just pass one object with whichever fields you need.

<MhCalendar config={{ viewType: 'MONTH', availableViews: ['MONTH', 'WEEK', 'AGENDA'] }} events={events} />

config and events are one-way: they describe what the calendar should render. To read the calendar's current state or imperatively navigate/mutate events, use the getApi() method exposed on the element instead.

events

events: IMHCalendarEvent[] is the list of events to render.

interface IMHCalendarEvent {
id: string;
startDate: Date;
endDate: Date;
title?: string;
allDay?: boolean;
description?: string;
isHidden?: boolean;
color?: string;
resourceId?: string; // matches a resource's `id` in the Resource view, see Resources
draggingToggle?: boolean;
[key: string]: unknown; // extra fields are allowed and passed through
}

A multi-day event is duplicated across each date it spans internally for rendering, you don't need to split it yourself; pass one object with startDate/endDate and let the calendar handle the rest.

This prop is one-way: the calendar never mutates the array you pass in. Creating, dragging, or resizing an event fires a callback (see Click callbacks and onEventCreated/onEventUpdated) instead of changing your data directly, persist the change yourself and pass an updated array back down.

const [events, setEvents] = useState<IMHCalendarEvent[]>(INITIAL_EVENTS);

<MhCalendar config={config} events={events} />;

If you'd rather mutate events imperatively instead of reacting to callbacks, getApi() exposes addEvent, updateEvent, and removeEvent directly.

For a walkthrough of the everyday fields (title, color, hiding, blocking drag on one event), see Displaying events.

viewType / availableViews

PropTypeDefaultDescription
viewType'DAY' | 'WEEK' | 'MONTH' | 'AGENDA' | 'RESOURCE''AGENDA'The active view.
availableViewsstring[]all viewsRestricts which views the built-in view switcher offers.

viewType controls which view renders. Users can also switch views themselves via the built-in navigation bar (toggle it with showCalendarNavigation); restrict which views are offered with availableViews.

<MhCalendar config={{ viewType: 'MONTH', availableViews: ['MONTH', 'WEEK', 'AGENDA'] }} events={events} />

startDate

PropTypeDefaultDescription
startDateDate | stringtodayInitial anchor date shown on mount.

Only read once, on mount: it's the initial date, not a controlled value. To navigate programmatically afterwards (next/previous period, jump to today), use the getApi() method exposed on the element instead.

Four independent booleans that toggle pieces of the built-in chrome. All default to true.

PropTypeDefaultDescription
showDateSwitcherbooleantrueShows the prev/next/today controls.
showViewTypeSwitcherbooleantrueShows the view-switch buttons/select.
showCalendarNavigationbooleantrueToggles the whole top navigation bar (date switcher + title + view switcher).
showViewHeaderbooleantrueShows the per-view header row (day names, dates).

showCalendarNavigation is the master switch for the whole bar: turning it off hides the date switcher and view switcher regardless of their own settings. Use the other two when you want the bar itself but need to hide just one control, e.g. building your own view switcher and hiding only showViewTypeSwitcher.

<MhCalendar config={{ showCalendarNavigation: false }} events={events} />

allowEventDragging / allowEventResize / minEventDuration

PropTypeDefaultDescription
allowEventDraggingbooleantrueEnables drag-to-move for events.
allowEventResizebooleantrueEnables drag-to-resize on event edges.
minEventDurationnumber (minutes)15Minimum duration enforced when resizing.
const config = {
allowEventDragging: true,
allowEventResize: true,
minEventDuration: 30,
};

Important: these props only control whether the interaction is allowed, they don't persist the result. Drag/resize fires onEventUpdated, and the calendar never mutates the events array you passed in. If you don't update your own state inside that callback, the change visually animates and then snaps back on the next render. See onEventCreated/onEventUpdated.

Dragging/resizing into a blocked time range is a separate concern, see Business hours.

createEventOnClick

PropTypeDefaultDescription
createEventOnClickbooleanfalseClicking an empty slot creates a new event (fires onEventCreated).

Must be explicitly enabled: without it, onDayClick still fires on an empty-slot click, but no event is created for you.

const [events, setEvents] = useState<IMHCalendarEvent[]>(INITIAL_EVENTS);

const config = {
createEventOnClick: true,
onEventCreated: (event) => setEvents((prev) => [...prev, event]),
};

As with drag/resize, the calendar doesn't add the event to your data for you; persist it yourself inside onEventCreated, see onEventCreated/onEventUpdated.

fixedHeight / virtualScrollHeight

PropTypeDefaultDescription
fixedHeightstringnoneEnables virtual scrolling; must be set together with virtualScrollHeight.
virtualScrollHeightstringnoneTotal scrollable height when fixedHeight is set.

Both must be set together, setting only one throws immediately when config is applied:

Both fixedHeight and virtualScrollHeight must be set for virtual scrolling to work.

Useful for rendering a very tall time range (e.g. midnight to midnight) without paying the render cost of every slot up front. See Display hours for more on controlling the displayed range.

theme

PropTypeDefaultDescription
theme'dark' | 'light' | string'dark'Base theme preset; anything other than dark/light falls back to the dark theme.

Picks the base defaults your style overrides are layered on top of.

const config = { theme: 'light', style: { properties: { eventBackgroundColor: '#1e3a8a' } } };

See Styling for the full property reference and Theming for packaging overrides into a reusable, named theme.

style

PropTypeDefaultDescription
style{ properties?, styles? }{}CSS custom properties (properties) plus per-class inline overrides keyed by class name (styles).
const config = {
style: {
properties: { eventBackgroundColor: '#1e3a8a', bordersColor: '#cbd5e1' },
styles: { mhCalendarEvent: { borderRadius: '10px' } },
},
};

Every component renders with shadow: false: there's no Shadow DOM boundary and no ::part() selectors to learn, so style (custom properties plus per-class overrides) is the only styling surface. This is big enough to warrant its own section, see Styling for the full custom-property table and class list, and Theming for reusing a style object as a named theme.

eventContent / eventSmallContent

PropTypeDefaultDescription
eventContent(event) => anynoneCustom render for full-size events (Day/Week views).
eventSmallContent(event) => anynoneCustom render for compact events (Month/Resource/Agenda).

Both receive the IMHCalendarEvent object (see events) and return renderable content:

const config = {
eventContent: (event) => (
<div className="my-event">
<strong>{event.title}</strong>
{event.description && <p>{event.description}</p>}
</div>
),
eventSmallContent: (event) => <span>{event.title}</span>,
};

If you only need to change colors per event rather than the whole markup, it's usually simpler to set event.color on individual events instead of overriding rendering, see events.

Translated event text (title, description, or any extra field) is entirely in your control through these two props, see Localization for the current state of i18n more broadly.

Click callbacks

PropTypeFires when
onEventClick(event: IMHCalendarEvent) => voidLeft-click on an event.
onRightEventClick(event: IMHCalendarEvent) => voidRight-click (context menu) on an event.
onDayClick(day: IMHCalendarDayClickPayload) => voidLeft-click on an empty day/slot.
onRightDayClick(day: IMHCalendarDayClickPayload) => voidRight-click on an empty day/slot.

IMHCalendarDayClickPayload:

type IMHCalendarDayClickPayload = {
date: Date;
resourceId?: string; // present for Resource clicks
};

onDayClick fires on every empty-slot click regardless of createEventOnClick, it doesn't create an event by itself, it just tells you where the click happened.

const config = {
onEventClick: (event) => openDetails(event.id),
onDayClick: (day) => console.log(day.date, day.resourceId),
};

onEventCreated / onEventUpdated

PropTypeFires when
onEventCreated(event: IMHCalendarEvent) => voidA new event was created by clicking an empty slot (requires createEventOnClick: true).
onEventUpdated(event: IMHCalendarEvent) => voidAn existing event finished being dragged or resized (see allowEventDragging/allowEventResize).

Important: these callbacks tell you what happened, they don't mutate your data. The calendar never changes the events array you passed in. To actually persist a create/drag/resize, update your own state inside the callback:

const [events, setEvents] = useState<IMHCalendarEvent[]>(INITIAL_EVENTS);

const config = {
createEventOnClick: true,
allowEventDragging: true,
allowEventResize: true,
onEventCreated: (event) => setEvents((prev) => [...prev, event]),
onEventUpdated: (updated) =>
setEvents((prev) => prev.map((e) => (e.id === updated.id ? updated : e))),
};

<MhCalendar config={config} events={events} />;

If you skip this, drag/resize/create will visually animate but snap back: the calendar re-renders from whatever events array you continue to pass it.

If you'd rather mutate events imperatively than react to these callbacks, getApi() exposes addEvent/updateEvent/removeEvent directly.

showTimeFrom / showTimeTo

PropTypeDefaultDescription
showTimeFromnumber8First visible hour (0 to 24).
showTimeTonumber17Last visible hour; must be greater than showTimeFrom.
<MhCalendar config={{ showTimeFrom: 8, showTimeTo: 18 }} events={events} />

showTimeTo must be strictly greater than showTimeFrom, otherwise config validation throws immediately. See Display hours for a full walkthrough, including rendering a very tall range with fixedHeight/virtualScrollHeight.

slotInterval / hoursSlotInterval / hoursDisplayFormat

PropTypeDefaultDescription
slotInterval{ hours, minutes, visibleEvery? }{ hours: 1, minutes: 0 }The invisible snapping grid used when dragging and resizing events.
hoursSlotInterval{ hours, minutes }{ hours: 1, minutes: 0 }Granularity of the hour-label column.
hoursDisplayFormatstring'h A'Day.js format string for hour labels, e.g. 'HH:mm' for 24-hour time.

minutes on slotInterval/hoursSlotInterval must be divisible by 5, otherwise config validation throws immediately.

Events snap to slotInterval when dragged or resized, this can't be disabled, but you can reduce it down to { hours: 0, minutes: 1 } if you want movement to feel almost unrestricted while still snapping. hoursSlotInterval is purely cosmetic: it controls how many hour labels show in the time gutter without affecting the actual snapping grid or displayed range.

const config = {
slotInterval: { hours: 0, minutes: 15 },
hoursSlotInterval: { hours: 2, minutes: 0 },
hoursDisplayFormat: 'HH:mm',
};

See Display hours for more on combining these.

showAllDayTasks / allDayEventsHeight

PropTypeDefaultDescription
showAllDayTasksbooleantrueShows the all-day event row.
allDayEventsHeightnumber100Pixel height of the all-day row.

Events with allDay: true on their IMHCalendarEvent render in this dedicated row instead of the time grid.

const config = { showAllDayTasks: true, allDayEventsHeight: 60 };

makeAllDaysSticky

PropTypeDefaultDescription
makeAllDaysStickybooleanfalseSticks the day header row on scroll.
const config = { makeAllDaysSticky: true };

hiddenDays

PropTypeDefaultDescription
hiddenDaysnumber[][]Days to hide, 0 = Sunday through 6 = Saturday.
const config = { hiddenDays: [0, 6] }; // hide weekends

Business hours

Defines the active time window for event placement. When set, areas outside this range are visually disabled (shaded gray by default). Useful for constraining event creation within a broader displayed time range (e.g. displaying 8:00 to 18:00, but allowing events only between 10:00 and 16:00).

<MhCalendar
config={{
businessHours: [
{ dayOfWeek: [1, 2, 3, 4, 5], start: 9, end: 17 }, // Monday to Friday
{ dayOfWeek: [0, 6], start: 10, end: 14 }, // Weekend, shorter hours
],
}}
/>

Each entry in the array is a BusinessHoursConfig:

FieldTypeDescription
dayOfWeeknumber | number[]0 (Sunday) to 6 (Saturday). Omit to match any day not already matched by a more specific entry.
dateDate | stringA specific calendar date. Takes priority over dayOfWeek for that day.
startnumberOpening hour (0 to 23).
endnumberClosing hour (0 to 24).

How business hours are resolved

businessHours lets you set both recurring weekly schedules (using dayOfWeek) and specific date overrides (using date).

For any given day, the calendar evaluates the businessHours array from top to bottom and applies the first matching entry based on this priority order:

  1. Specific date (date). Takes highest priority, overrides any recurring weekly rules for that specific calendar day.
  2. Day of week (dayOfWeek). Matches recurring days of the week (0 for Sunday through 6 for Saturday).
<MhCalendar
config={{
businessHours: [
{ date: '2026-12-24', start: 10, end: 14 }, // Early closure on Christmas Eve
{ dayOfWeek: [1, 2, 3, 4, 5], start: 9, end: 17 }, // Mon-Fri
{ dayOfWeek: [0, 6], start: 10, end: 14 }, // weekend
],
}}
/>

Blocking drops outside business hours

By default, dragging or resizing an event into non-business hours is allowed, the gray overlay is purely visual. Set blockBusinessHours: true to actually prevent it.

<MhCalendar
config={{
businessHours: [{ dayOfWeek: [1, 2, 3, 4, 5], start: 9, end: 17 }],
blockBusinessHours: true,
}}
/>

Styling the overlay

The overlay tint on non-business hours is controlled by the nonBusinessHoursOverlayColor CSS custom property, see Styling → Properties.

<MhCalendar
config={{
style: { properties: { nonBusinessHoursOverlayColor: 'rgba(220, 38, 38, 0.06)' } },
}}
/>

timezones / timezoneLabel

PropTypeDefaultDescription
timezonesstring[][]Up to 3 IANA timezone names; first is the main timezone.
timezoneLabelstringautoOverrides the auto-generated main timezone label (e.g. "CET (GMT+1)").
const config = {
timezones: ['America/Los_Angeles', 'America/New_York', 'Europe/London'],
timezoneLabel: 'HQ',
};
  • Accepts IANA timezone names (e.g. 'Europe/Warsaw'), not abbreviations or UTC offsets, an invalid string throws immediately when config is set.
  • Passing more than 3 entries doesn't throw; it logs a console.warn and keeps only the first three.
  • If timezones is omitted, the calendar falls back to the browser's own timezone.

See Timezones for the full guide, including why events themselves stay timezone-agnostic.

locale / labels

PropTypeDefaultDescription
localestring | ILocale'en'Day.js locale used to format day/month names (e.g. 'ddd', 'MMMM').
labelsPartial<IMHCalendarLabels>undefinedOverrides for hardcoded UI strings (the "Today" button/label, the "+N more" overflow indicator, and view switcher names).
import plLocale from 'dayjs/locale/pl';

const config = {
locale: plLocale,
labels: {
today: 'Dzisiaj',
moreEvents: (hiddenCount) => `+${hiddenCount} więcej`,
views: { MONTH: 'Miesiąc', WEEK: 'Tydzień' },
},
};

locale: pass the imported Day.js locale object, not its BCP 47 tag as a bare string (e.g. not locale: 'pl' with a side-effect-only import 'dayjs/locale/pl'). This package bundles its own private Day.js instance, so a side-effect import in your app registers the locale on a different Day.js instance and silently has no effect here — passing the object itself works regardless, because Day.js self-registers whatever locale object it's given at the point of use. Each calendar instance formats with its own locale value instead of mutating Day.js's global locale, so multiple instances on the same page can use different locales safely. The default 'en' is the one case where the plain string is fine, since it's Day.js's built-in fallback.

labels: any key you omit falls back to the built-in English default (e.g. 'Today', `+${hiddenCount} more`, or the title-cased view type). views only needs entries for the views you want to relabel.

eventDisplayMode

PropTypeDefaultDescription
eventDisplayMode'side-by-side' | 'overlapping''side-by-side'How concurrent events lay out within a day column.
const config = { eventDisplayMode: 'overlapping' };

showTimeIndicator

PropTypeDefaultDescription
showTimeIndicatorbooleantrueShows the current-time red line.
const config = { showTimeIndicator: false };

The line's color is controlled by the currentTimeColor CSS custom property, see Styling → Properties.

resources

PropTypeDefaultDescription
resources{ id: string; title: string }[][]Rows for the Resource view; matched to events via event.resourceId.
const config = {
viewType: 'RESOURCE',
resources: [
{ id: 'alice', title: 'Alice' },
{ id: 'bob', title: 'Bob' },
],
};

const events: IMHCalendarEvent[] = [
{ id: 's1', resourceId: 'alice', title: 'Morning shift', startDate, endDate },
];

resourceId on an event (see events) is also read by onDayClick/onRightDayClick's payload when clicking an empty Resource cell.

resourceDays

PropTypeDefaultDescription
resourceDaysnumber7Number of days shown in the Resource view.
const config = { viewType: 'RESOURCE', resourceDays: 14 };