Calendar
A full-featured, plugin-extensible calendar component with 8 view modes, drag-and-drop, custom rendering, and a comprehensive imperative API.
Basic Calendar
A fully interactive calendar with default configuration.
import { Calendar } from 'fluxo-ui';
import type { CalendarEntry } from 'fluxo-ui';
const entries: CalendarEntry[] = [
{
id: '1',
title: 'Team Standup',
start: new Date(2025, 8, 15, 9, 0),
end: new Date(2025, 8, 15, 9, 30),
color: '#3b82f6',
},
{
id: '2',
title: 'Sprint Planning',
start: new Date(2025, 8, 15, 10, 0),
end: new Date(2025, 8, 15, 11, 30),
color: '#8b5cf6',
editable: true,
},
];
<Calendar
entries={entries}
initialView="timeGridWeek"
height={600}
editable
selectable
nowIndicator
showNavigationPicker
/>View Modes
All 8 built-in view modes. Click buttons to switch.
Custom Entry Rendering
Use renderEntry to fully control entry appearance with icons and custom layouts.
<Calendar
entries={entries}
renderEntry={(entry, context) => (
<div style={{ padding: '2px 4px' }}>
<strong>{entry.title}</strong>
{!context.isCompact && (
<div style={{ fontSize: '0.7rem', opacity: 0.8 }}>
{entry.data?.location}
</div>
)}
</div>
)}
/>Time Grid Configuration
Configure slot duration, visible hours, business hours, time format, and more.
<Calendar
entries={entries}
slotDuration={15}
visibleHoursStart={8}
visibleHoursEnd={20}
firstDayOfWeek={1}
timeFormat="24h"
businessHours={{
daysOfWeek: [1, 2, 3, 4, 5],
startTime: '09:00',
endTime: '17:00',
}}
hiddenDays={[0, 6]}
rowBanding
nowIndicator
navLinks
/>Drag, Drop & Resize
Entries marked as editable can be moved and resized. The fixed entry cannot be moved.
<Calendar
entries={entries}
onEntryClick={(entry, event) => {
console.log('Clicked:', entry.title);
}}
onDateSelect={(info, event) => {
console.log('Selected:', info.start, '-', info.end);
}}
onEntryDrop={(info, event) => {
console.log('Moved:', info.entry.title, 'to', info.newStart);
}}
onEntryResize={(info, event) => {
console.log('Resized:', info.entry.title);
}}
onEntryContextMenu={(entry, event) => {
console.log('Right-click:', entry.title);
}}
/>Imperative API
Control the calendar programmatically using a ref.
import { useRef } from 'react';
import { Calendar } from 'fluxo-ui';
import type { CalendarApi } from 'fluxo-ui';
const MyComponent = () => {
const calendarRef = useRef<CalendarApi>(null);
return (
<>
<button onClick={() => calendarRef.current?.prev()}>
Previous
</button>
<button onClick={() => calendarRef.current?.today()}>
Today
</button>
<button onClick={() => calendarRef.current?.changeView('dayGridMonth')}>
Month View
</button>
<Calendar
entries={entries}
apiRef={calendarRef}
/>
</>
);
};Plugin-Based Views
Views are tree-shakeable plugins. Only import what you need — unused views won't be bundled. New views: Year Grid, Agenda, and custom N-day time grids.
import {
Calendar,
timeGridPlugin, monthGridPlugin,
yearGridPlugin, agendaPlugin,
timeGridCustomPlugin,
} from 'fluxo-ui';
// Only bundle the views you need
const plugins = [
timeGridPlugin(), // timeGridWeek + timeGridDay
monthGridPlugin(), // dayGridMonth
yearGridPlugin(), // yearGrid (12-month overview)
agendaPlugin({ dayCount: 14 }), // 2-week agenda
timeGridCustomPlugin({ dayCount: 3, label: '3 Days' }),
];
<Calendar
plugins={plugins}
initialView="timeGridWeek"
entries={entries}
/>// Only show specific views in the toolbar dropdown
<Calendar
headerToolbarViews={['timeGridWeek', 'dayGridMonth', 'yearGrid']}
entries={entries}
/>import { createViewPlugin, defineView } from 'fluxo-ui';
// Build your own view
const myView = defineView({
name: 'myCustomView',
label: 'My View',
component: MyViewComponent, // React component with ViewProps
getDateRange: (date, firstDayOfWeek) => ({ start, end }),
getTitle: (range) => 'My Custom Title',
navigate: (date, dir) =>
dir === 'prev' ? subDays(date, 7) : addDays(date, 7),
});
const myPlugin = createViewPlugin({
name: 'my-plugin',
views: [myView],
});
<Calendar plugins={[myPlugin]} initialView="myCustomView" />Plugin System
Extend the calendar with custom plugins that add toolbar actions, views, and entry renderers.
import { Calendar } from 'fluxo-ui';
import type { CalendarPlugin, CalendarViewDefinition } from 'fluxo-ui';
const myPlugin: CalendarPlugin = {
name: 'my-custom-plugin',
views: [myCustomView],
toolbarActions: [
{
id: 'export',
label: 'Export',
position: 'end',
onClick: (api) => {
const entries = api.getEntries();
console.log('Exporting', entries.length, 'entries');
},
},
],
onInit: (api) => console.log('Plugin initialized'),
};
<Calendar
entries={entries}
plugins={[myPlugin]}
/>External Drag & Drop
Drag items from the sidebar into the calendar to create new entries.
Drag these into calendar
<Calendar
entries={entries}
editable
onExternalDrop={(info) => {
console.log('Dropped at', info.date, 'allDay:', info.allDay);
// Create new entry from the dropped data
}}
/>Date Backgrounds
Highlight specific dates or date ranges with custom background colors. Works across all views.
<Calendar
entries={entries}
dateBackgrounds={[
{ date: new Date(2026, 3, 8), color: 'rgba(59, 130, 246, 0.1)' },
{ date: new Date(2026, 3, 10), color: 'rgba(239, 68, 68, 0.1)' },
]}
dateRangeBackgrounds={[
{ start: new Date(2026, 3, 5), end: new Date(2026, 3, 7), color: 'rgba(139, 92, 246, 0.08)' },
]}
/>Custom Toolbar End
Use renderToolbarEnd to compose the right side of the toolbar with your own components alongside the built-in view switcher and plugin actions.
import { Calendar, ToolbarEndRenderProps } from 'fluxo-ui';
<Calendar
entries={entries}
initialView="timeGridWeek"
showNavigationPicker
renderToolbarEnd={({ viewSwitcher, pluginActions }) => (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<button onClick={() => alert('Custom action!')}>+ New Event</button>
{pluginActions}
{viewSwitcher}
</div>
)}
/>
// The renderToolbarEnd callback receives:
// - viewSwitcher: The view mode dropdown (ReactNode)
// - pluginActions: Plugin toolbar actions for the end slot (ReactNode)
// Compose them in any order alongside your own components.Icon-Only Navigation Picker
With navigationPickerIconOnly, the title remains visible as the header while a compact calendar icon provides quick date navigation.
<Calendar
entries={entries}
initialView="timeGridWeek"
showNavigationPicker
navigationPickerIconOnly
/>
// With navigationPickerIconOnly:
// - Title text remains visible as the header
// - A small calendar icon appears next to nav buttons
// to open the date picker popover
// Without navigationPickerIconOnly (default):
// - The date range picker replaces the title,
// styled as the header text with a picker iconCompact & Embedded Mode
Use compact mode for embedding in sidebars or dashboards. Hide toolbar sections as needed.
Compact Mode
Hidden Toolbar Sections
Props
entriesCalendarEntry[]"[]"Array of calendar entries to display
entriesCalendarEntry[]"[]"Array of calendar entries to display
initialViewCalendarViewMode"timeGridWeek"Starting view mode
initialViewCalendarViewMode"timeGridWeek"Starting view mode
initialDateDate | string"new Date()"Starting date for navigation
initialDateDate | string"new Date()"Starting date for navigation
pluginsCalendarPlugin[]"defaultPlugins"Array of view/feature plugins. When omitted, all built-in views are registered. Pass specific plugins to tree-shake unused views.
pluginsCalendarPlugin[]"defaultPlugins"Array of view/feature plugins. When omitted, all built-in views are registered. Pass specific plugins to tree-shake unused views.
headerToolbarViewsstring[]Restrict which view modes appear in the toolbar dropdown. Array of view name strings (e.g. [\"timeGridWeek\", \"dayGridMonth\"]). When omitted, all registered views are shown.
headerToolbarViewsstring[]Restrict which view modes appear in the toolbar dropdown. Array of view name strings (e.g. [\"timeGridWeek\", \"dayGridMonth\"]). When omitted, all registered views are shown.
slotDurationnumber"30"Time grid slot interval in minutes
slotDurationnumber"30"Time grid slot interval in minutes
visibleHoursStartnumber"0"First visible hour (0-23)
visibleHoursStartnumber"0"First visible hour (0-23)
visibleHoursEndnumber"24"Last visible hour (1-24)
visibleHoursEndnumber"24"Last visible hour (1-24)
businessHoursBusinessHours"Mon-Fri 9-17"Working days and hours config
businessHoursBusinessHours"Mon-Fri 9-17"Working days and hours config
firstDayOfWeeknumber"0"Week start day (0=Sun, 1=Mon)
firstDayOfWeeknumber"0"Week start day (0=Sun, 1=Mon)
hiddenDaysnumber[]"[]"Days to hide from views
hiddenDaysnumber[]"[]"Days to hide from views
timeFormat'12h' | '24h'"12h"Time display format
timeFormat'12h' | '24h'"12h"Time display format
editableboolean"false"Global drag/drop/resize default
editableboolean"false"Global drag/drop/resize default
selectableboolean"true"Enable time range selection
selectableboolean"true"Enable time range selection
creatableboolean"false"Enable entry creation via drag (time grid) or double-click (month/day grid). Fires onEntryCreate callback.
creatableboolean"false"Enable entry creation via drag (time grid) or double-click (month/day grid). Fires onEntryCreate callback.
slotHeightnumber"24"Height in pixels of each time slot in time grid views. Lower values make the grid more compact.
slotHeightnumber"24"Height in pixels of each time slot in time grid views. Lower values make the grid more compact.
onEntryCreate(info: EntryCreateInfo) => voidCalled when a new entry is created via drag or double-click. Receives { start, end, allDay, view }.
onEntryCreate(info: EntryCreateInfo) => voidCalled when a new entry is created via drag or double-click. Receives { start, end, allDay, view }.
eventMinDurationnumber"0"Minimum entry duration in minutes when creating/resizing (0 = no limit).
eventMinDurationnumber"0"Minimum entry duration in minutes when creating/resizing (0 = no limit).
eventMaxDurationnumber"0"Maximum entry duration in minutes when creating/resizing (0 = no limit).
eventMaxDurationnumber"0"Maximum entry duration in minutes when creating/resizing (0 = no limit).
eventDefaultDurationnumber"60"Default duration in minutes for entries created via click (not drag).
eventDefaultDurationnumber"60"Default duration in minutes for entries created via click (not drag).
eventDurationEditableboolean"true"Whether entries can be resized. Separate from editable (move).
eventDurationEditableboolean"true"Whether entries can be resized. Separate from editable (move).
eventStartEditableboolean"true"Whether entries can be moved. Separate from editable (resize).
eventStartEditableboolean"true"Whether entries can be moved. Separate from editable (resize).
eventOverlapboolean"true"Whether dragged/resized entries can overlap existing entries.
eventOverlapboolean"true"Whether dragged/resized entries can overlap existing entries.
eventClassNames(entry) => string | string[]Dynamic CSS class names per entry for conditional styling.
eventClassNames(entry) => string | string[]Dynamic CSS class names per entry for conditional styling.
eventConstraint(info) => booleanRestrict event drag/resize to specific time ranges (e.g., only within business hours).
eventConstraint(info) => booleanRestrict event drag/resize to specific time ranges (e.g., only within business hours).
eventDataTransform(entry) => CalendarEntryTransform function applied to all entry data before rendering.
eventDataTransform(entry) => CalendarEntryTransform function applied to all entry data before rendering.
entryOrder(a, b) => numberCustom sort function for ordering overlapping entries.
entryOrder(a, b) => numberCustom sort function for ordering overlapping entries.
selectMinDistancenumber"0"Min drag distance in pixels before selection starts.
selectMinDistancenumber"0"Min drag distance in pixels before selection starts.
selectOverlapboolean"true"Whether selections can overlap existing entries.
selectOverlapboolean"true"Whether selections can overlap existing entries.
selectAllow(info) => booleanConditionally prevent selection of certain time ranges.
selectAllow(info) => booleanConditionally prevent selection of certain time ranges.
selectMirrorboolean"false"Show a mirror entry during drag-to-create instead of selection highlight.
selectMirrorboolean"false"Show a mirror entry during drag-to-create instead of selection highlight.
eventAllow(info) => booleanConditionally prevent entry drop/resize at certain times.
eventAllow(info) => booleanConditionally prevent entry drop/resize at certain times.
scrollTimestring"08:00"Initial scroll position for time grid views (HH:MM format).
scrollTimestring"08:00"Initial scroll position for time grid views (HH:MM format).
longPressDelaynumber"1000"Milliseconds to hold before touch becomes drag on mobile.
longPressDelaynumber"1000"Milliseconds to hold before touch becomes drag on mobile.
moreLinkClick'popover' | 'day' | callback"popover"Behavior when +N more is clicked in month view.
moreLinkClick'popover' | 'day' | callback"popover"Behavior when +N more is clicked in month view.
weekNumberClick(weekNum, date, event) => voidCallback when a week number is clicked.
weekNumberClick(weekNum, date, event) => voidCallback when a week number is clicked.
dayHeaderClick(date, event) => voidCallback when a day column header is clicked in time grid.
dayHeaderClick(date, event) => voidCallback when a day column header is clicked in time grid.
dateAlignmentnumber"0"Navigate by N days instead of view-width (0 = use view default).
dateAlignmentnumber"0"Navigate by N days instead of view-width (0 = use view default).
allDaySlotMaxHeightnumber"0"Max height in px for the all-day row (0 = unlimited).
allDaySlotMaxHeightnumber"0"Max height in px for the all-day row (0 = unlimited).
dayMinWidthnumber"0"Min column width in px for day columns in time grid (0 = auto).
dayMinWidthnumber"0"Min column width in px for day columns in time grid (0 = auto).
dayMinHeightnumber"0"Min height in px for day cells in month view (0 = auto).
dayMinHeightnumber"0"Min height in px for day cells in month view (0 = auto).
slotLabelIntervalnumber"0"Interval in minutes at which slot labels appear in the time gutter. 0 (default) = label only at the start of each hour. Must be a positive divisor of 60 (e.g. 15, 30) when set. Use slotDuration to label every slot or 2*slotDuration for every other slot.
slotLabelIntervalnumber"0"Interval in minutes at which slot labels appear in the time gutter. 0 (default) = label only at the start of each hour. Must be a positive divisor of 60 (e.g. 15, 30) when set. Use slotDuration to label every slot or 2*slotDuration for every other slot.
loadingboolean"false"Show a loading indicator overlay on the calendar.
loadingboolean"false"Show a loading indicator overlay on the calendar.
stickyHeaderDatesboolean"true"Keep day/date headers sticky when scrolling the time grid.
stickyHeaderDatesboolean"true"Keep day/date headers sticky when scrolling the time grid.
weekTextstring"''"Prefix for week numbers display.
weekTextstring"''"Prefix for week numbers display.
fixedWeekCountboolean"true"Always show 6 weeks in month grid.
fixedWeekCountboolean"true"Always show 6 weeks in month grid.
expandRowsboolean"false"Expand month grid rows to fill available space.
expandRowsboolean"false"Expand month grid rows to fill available space.
multiMonthCountnumber"3"Number of months to display in multi-month view.
multiMonthCountnumber"3"Number of months to display in multi-month view.
nowIndicatorIntervalnumber"60000"Update frequency in ms for the now indicator line.
nowIndicatorIntervalnumber"60000"Update frequency in ms for the now indicator line.
dayPopoverFormatstring"'EEEE, MMMM d'"Date format in overflow popover header.
dayPopoverFormatstring"'EEEE, MMMM d'"Date format in overflow popover header.
allDayTextstring"'all-day'"Label text for the all-day row in time grid and overflow popover.
allDayTextstring"'all-day'"Label text for the all-day row in time grid and overflow popover.
displayEventTimeboolean"true"Whether to show the start time on entries in the default entry renderer.
displayEventTimeboolean"true"Whether to show the start time on entries in the default entry renderer.
nowIndicatorboolean"true"Show current time line
nowIndicatorboolean"true"Show current time line
navLinksboolean"true"Date headers as navigation links
navLinksboolean"true"Date headers as navigation links
compactboolean"false"Compact/embedded mode
compactboolean"false"Compact/embedded mode
rowBandingboolean"false"Alternate row colors on time grid
rowBandingboolean"false"Alternate row colors on time grid
showNavigationPickerboolean"false"Show a DateRangePicker in the toolbar for quick date navigation. Auto-selects week/month/day mode based on current view.
showNavigationPickerboolean"false"Show a DateRangePicker in the toolbar for quick date navigation. Auto-selects week/month/day mode based on current view.
navigationPickerIconOnlyboolean"false"When true, shows only a calendar icon for the navigation picker (title remains visible). When false (default), the picker replaces the title with header-styled text.
navigationPickerIconOnlyboolean"false"When true, shows only a calendar icon for the navigation picker (title remains visible). When false (default), the picker replaces the title with header-styled text.
renderToolbarEnd(components) => ReactNodeCustom render for the toolbar end section. Receives { viewSwitcher, pluginActions } as ReactNodes to compose alongside custom components.
renderToolbarEnd(components) => ReactNodeCustom render for the toolbar end section. Receives { viewSwitcher, pluginActions } as ReactNodes to compose alongside custom components.
dateBackgroundsDateBackground[]Highlight specific dates with background colors. Array of {date, color}.
dateBackgroundsDateBackground[]Highlight specific dates with background colors. Array of {date, color}.
dateRangeBackgroundsDateRangeBackground[]Highlight date ranges with background colors. Array of {start, end, color}.
dateRangeBackgroundsDateRangeBackground[]Highlight date ranges with background colors. Array of {start, end, color}.
loadingRangesDateLoadingRange[]Show shimmer loaders for dates being loaded. Array of {start, end}.
loadingRangesDateLoadingRange[]Show shimmer loaders for dates being loaded. Array of {start, end}.
hideEmptyDaysboolean"false"List view only: when true, days that contain no entries are filtered out so users only see days with data. When all days in the range are empty, the empty state is rendered instead.
hideEmptyDaysboolean"false"List view only: when true, days that contain no entries are filtered out so users only see days with data. When all days in the range are empty, the empty state is rendered instead.
emptyMessageReactNode"'No entries to display'"Message shown when the entire list view has zero entries for the visible range. Pass a string or any ReactNode for richer content.
emptyMessageReactNode"'No entries to display'"Message shown when the entire list view has zero entries for the visible range. Pass a string or any ReactNode for richer content.
emptyDayMessageReactNode"'No entries'"Inline message inside a day group when that day is empty. Only shown when hideEmptyDays is false.
emptyDayMessageReactNode"'No entries'"Inline message inside a day group when that day is empty. Only shown when hideEmptyDays is false.
renderEmpty(info: { dateRange }) => ReactNodeRender-prop for a fully custom empty state. When provided, overrides the default icon + emptyMessage.
renderEmpty(info: { dateRange }) => ReactNodeRender-prop for a fully custom empty state. When provided, overrides the default icon + emptyMessage.
showAllDayRow'auto' | 'always'"auto"Time grid all-day row visibility. 'auto' (default) hides the row when there are no all-day entries. 'always' keeps the row visible so the gutter label and day slots remain available as drop targets / visual anchors.
showAllDayRow'auto' | 'always'"auto"Time grid all-day row visibility. 'auto' (default) hides the row when there are no all-day entries. 'always' keeps the row visible so the gutter label and day slots remain available as drop targets / visual anchors.
titleFormatstringCustom date-fns format string for the toolbar title.
titleFormatstringCustom date-fns format string for the toolbar title.
dayHeaderFormatstring"d"Format for day numbers in headers.
dayHeaderFormatstring"d"Format for day numbers in headers.
weekDayHeaderFormatstring"EEE"Format for weekday names in headers.
weekDayHeaderFormatstring"EEE"Format for weekday names in headers.
dayHeaderLayout'stacked' | 'inline'"stacked"Layout for time-grid day headers. 'stacked' shows weekday on top and large day-number below (modern, spacious). 'inline' shows them on a single compact line — when dayHeaderFormat is provided it is used verbatim (e.g. 'EEE, dd/MM' → 'Sun, 17/05'); otherwise weekday + day-number are joined.
dayHeaderLayout'stacked' | 'inline'"stacked"Layout for time-grid day headers. 'stacked' shows weekday on top and large day-number below (modern, spacious). 'inline' shows them on a single compact line — when dayHeaderFormat is provided it is used verbatim (e.g. 'EEE, dd/MM' → 'Sun, 17/05'); otherwise weekday + day-number are joined.
onExternalDrop(info, event) => voidCalled when an external item is dropped onto the calendar.
onExternalDrop(info, event) => voidCalled when an external item is dropped onto the calendar.
heightstring | number"100%"Calendar container height
heightstring | number"100%"Calendar container height
renderEntry(entry, context) => ReactNodeCustom entry template
renderEntry(entry, context) => ReactNodeCustom entry template
renderToolbar(defaults, api) => ReactNodeCustom toolbar rendering
renderToolbar(defaults, api) => ReactNodeCustom toolbar rendering
onEntryClick(entry, event) => voidEntry click callback
onEntryClick(entry, event) => voidEntry click callback
onDateSelect(info, event) => voidTime range selection callback
onDateSelect(info, event) => voidTime range selection callback
onEntryDrop(info, event) => voidDrag-drop callback
onEntryDrop(info, event) => voidDrag-drop callback
onEntryResize(info, event) => voidResize callback
onEntryResize(info, event) => voidResize callback
onViewChange(view) => voidView mode change callback
onViewChange(view) => voidView mode change callback
onDateRangeChange(range) => voidDate range change callback
onDateRangeChange(range) => voidDate range change callback
onTitleChange(title: string) => voidFires whenever the toolbar title changes (e.g. after navigation or a view change). Useful when the built-in toolbar is hidden (hideToolbar) and the host wants to render the title in its own header bar.
onTitleChange(title: string) => voidFires whenever the toolbar title changes (e.g. after navigation or a view change). Useful when the built-in toolbar is hidden (hideToolbar) and the host wants to render the title in its own header bar.
apiRefRefObject<CalendarApi>Ref for imperative API access
apiRefRefObject<CalendarApi>Ref for imperative API access
Features
13+ View Modes
Month, week, day, year, agenda, N-day, list, multi-month, scroll — all as tree-shakeable plugins
Plugin Architecture
Tree-shakeable view plugins, custom views via defineView/createViewPlugin, toolbar actions, entry renderers
Drag & Drop
Move and resize entries with snap-to-grid and modifier key support
Custom Rendering
Full control over entry appearance via render callbacks
Theming
Automatic support for all brand themes and dark mode via CSS variables
Business Hours
Visual distinction between work and non-work hours
Now Indicator
Real-time current time line on time grid views
Imperative API
Programmatic navigation, view switching, and entry access via ref
Keyboard Accessible
ARIA labels, focus management, and keyboard navigation
All-Day Entries
Dedicated all-day row in time grid views
Responsive
Mobile-friendly with adaptive layouts and touch support
Configurable Grid
Slot duration, visible hours, first day of week, hidden days