Calendar
A date and event view for schedules, with Month, Week, and Day modes.
Parked —
Calendarleft the root export in1.0.0and now ships fromfrappe-ui/experimentalwith its public API unchanged. It stays there, exempt from the deprecation policy, until a redesigned calendar family replaces it.
import { Calendar } from 'frappe-ui/experimental'
import type { CalendarEvent, CalendarConfig } from 'frappe-ui/experimental'Default
Custom Header
Pass a #header slot to replace the default toolbar. The slot receives the current title (currentMonthYear), the active view (activeView), the enabled view options (enabledModes), and navigation functions (increment, decrement, updateActiveView, setCalendarDate, onMonthYearChange).
Events
Each entry in events is a CalendarEvent:
{
id: 'EV-001',
title: 'Design review',
participant: 'Jane Doe',
venue: 'Room 1',
fromDate: '2026-08-10',
toDate: '2026-08-10',
fromTime: '10:00',
toTime: '11:00',
color: 'blue', // amber | violet | pink | cyan | blue | orange | green
isFullDay: false,
}An event runs from fromDate fromTime to toDate toTime, dates inclusive. One whose toDate is later than its fromDate spans those days: the Month view draws it as a single bar across them, and the Week view puts it in the all-day row. A timed event that crosses midnight but is shorter than a day (an evening running late) stays in the time grid, cut at midnight into a piece per day. A timed event ending at 00:00 stops as that day begins, so it does not occupy it. Dragging a spanning event moves both ends together.
isFullDay events ignore their times and cover fromDate..toDate whole.
The calendar keeps an internal copy of events and refreshes it when the prop changes. Edits made inside the calendar (create, drag, resize, delete) mutate the copy and come back through the create, update, and delete emits — persist them and refresh your source of truth from there.
CalendarColorMap exports the color palette (amber, violet, pink, cyan, blue, orange, green) with the CSS variables used per state, for building matching UI such as a color picker.
Month view
The Month view is a strip of week rows covering the month in view, and it scrolls when the rows outgrow the calendar's height. The arrows, Today, and the month picker move the month and scroll the strip to their date.
Each row is as tall as its busiest day needs. Multi-day events run as bars across the top of the row; single-day events sit beneath them in their cells with the title wrapping to a second line, so every event is shown — there is no "n more".
Below the sm breakpoint the days stack instead: a row per day, a heading where each month begins, and a week strip above to keep your place. Clicking a date number in either layout opens that day in the Day view.
rangeChange reports the strip's full extent for the Month view — the padding days of the first and last weeks included — so a data source that fetches by range has events for every cell.
Config
The config prop takes a partial CalendarConfig; unset keys use these defaults:
{
defaultMode: 'Month', // 'Day' | 'Week' | 'Month'
disableModes: [], // views removed from the view switcher
isEditMode: false, // create / drag / resize / delete
enableShortcuts: true, // keyboard shortcuts (below)
scrollToHour: 15, // hour Week and Day views scroll to
hourHeight: 50, // pixel height of one hour row
timeFormat: '12h', // '12h' | '24h'
weekends: ['sunday'], // days shaded as weekend
eventIcons: {}, // icons keyed by an event's `type`
showIcon: true, // show the eventIcons icon on cards
noBorder: false, // remove the outer grid border
}eventIcons values are Vue components.
Keyboard shortcuts
With enableShortcuts on: m / w / d switch views, t jumps to today, ← / → navigate, and Delete removes the event whose popover is open (edit mode only).
They stay out of the way of whatever is on top: nothing fires while a field has focus, or while a dialog, popover, menu or select is open anywhere on the page.
Click handling
By default, a single click on an event opens its detail popover and a double click opens the edit modal (edit mode only). Clicking an empty cell opens the new-event modal in edit mode. Each behavior is replaceable with the onClick, onDblClick, and onCellClick callback props — passing one turns the default off for that interaction.
The popover's content is replaceable with the #event-popover-content slot, which receives { calendarEvent, date, isEditMode, close }.
CalendarActiveEvent exports the ref holding the id of the event whose popover is open. Set it from outside to highlight an event, or clear it with an empty string. The ref is module-level: every <Calendar> on the page shares it.
Template ref
A template ref on <Calendar> exposes the calendar's state and navigation:
<Calendar ref="calendar" :events="events" />const calendar = useTemplateRef('calendar')
calendar.value.setCalendarDate('2026-08-10') // jump to a date (today if omitted)
calendar.value.updateActiveView('Week') // switch the view
calendar.value.activeView // the visible view
calendar.value.increment() // move forward one day / week / month
calendar.value.decrement() // move back one day / week / month
calendar.value.reloadEvents() // re-sync the internal copy of `events`
calendar.value.currentMonthYear // formatted title, e.g. "August 2026"
calendar.value.currentYear // 2026
calendar.value.currentMonth // 7 (0 = January)
calendar.value.currentDay // day of month anchoring the view
calendar.value.enabledModes // view options not disabled via config (fixed at mount)
calendar.value.selectedMonthDate // the month picker's date, `YYYY-MM-DD`
calendar.value.onMonthYearChange // jump to a date and sync the month pickerAPI Reference
Show types
import type { Component, InjectionKey, Ref } from 'vue'
export type CalendarMode = 'Day' | 'Week' | 'Month'
export type CalendarTimeFormat = '12h' | '24h'
export interface CalendarColor {
color: string
border: string
borderActive: string
text: string
textActive?: string
subtext: string
subtextActive: string
bg: string
bgHover: string
bgActive: string
}
export interface CalendarEvent {
id?: string | number
name?: string | number
title?: string
date?: string
/**
* First and last day, inclusive. An event whose `toDate` is later than
* its `fromDate` spans those days: the Month view draws it as one bar,
* the Week view puts it in the all-day row (or, for a timed event shorter
* than a day, splits it at midnight in the time grid).
*/
fromDate?: string
toDate?: string
fromTime?: string
/** A timed event ending at `00:00` on `toDate` stops as that day begins. */
toTime?: string
fromDateTime?: string
toDateTime?: string
participant?: string
venue?: string
color?: string
type?: string
/** Ignores the times and covers `fromDate`..`toDate` whole. */
isFullDay?: boolean
/** Saved but not sent: drawn as a dashed outline instead of a filled pill. */
isDraft?: boolean
/**
* The viewer said no. The title is struck through, and in the Week and Day
* views the event claims no room: overlapping events lay out as if it were
* not there and it sits full width beneath them.
*/
isDeclined?: boolean
startTime?: number
endTime?: number
hallNumber?: number
idx?: number
[key: string]: unknown
}
/**
* One day's piece of an event in the time grid. The `seg*` fields place the
* piece; the views set them and strip them again before an event leaves
* through an emit.
*/
export interface CalendarDaySegment extends CalendarEvent {
date: string
segFromTime: string
segToTime: string
/** Whether this piece is the event's first / last day. */
segIsStart: boolean
segIsEnd: boolean
}
/** An event clipped to one row of days and packed into a lane. */
export interface CalendarRowBar {
event: CalendarEvent
/** First and last column the bar covers, inclusive. */
startCol: number
endCol: number
/** Vertical slot; the same across every day of the row. */
lane: number
/** Whether the event begins / ends inside this row. */
isStart: boolean
isEnd: boolean
}
export interface CalendarConfig {
/** Hour (0-23) the Week and Day views scroll to on mount. */
scrollToHour: number
/** Views removed from the view switcher in the default header. */
disableModes: CalendarMode[]
/** View shown when the calendar mounts. */
defaultMode: CalendarMode
/**
* Enables editing: create events by clicking a cell, edit on double
* click, drag to move, resize, and delete with the keyboard.
*/
isEditMode: boolean
/** Icons keyed by an event's `type` field. */
eventIcons: Record<string, Component>
/** Pixel height of one hour row in the Week and Day views. */
hourHeight: number
/**
* Enables keyboard shortcuts: `m`/`w`/`d` switch views, `t` jumps to
* today, arrow keys navigate, Delete removes the open event.
*/
enableShortcuts: boolean
/** Shows the event's `eventIcons` icon on its card. */
showIcon: boolean
/** Clock format for time labels: `'12h'` or `'24h'`. */
timeFormat: CalendarTimeFormat
/**
* Days shaded as weekend. Weekday names (`'sunday'`) or indexes
* (0 = Sunday).
*/
weekends: string[]
/** Removes the outer grid border. */
noBorder?: boolean
}
export interface CalendarCellClickData {
e: MouseEvent
view: CalendarMode
date: Date | string
time: string
isFullDay: boolean
}
export interface CalendarPublicProps {
/** Events to render. Each needs an `id`, a title, and date/time fields. */
events: CalendarEvent[]
/** Behavior overrides, merged over the defaults. */
config?: Partial<CalendarConfig>
/**
* Replaces the default single-click behavior (opening the event
* popover) with your own handler.
*/
onClick?: (data: { e: MouseEvent; calendarEvent: CalendarEvent }) => void
/**
* Replaces the default double-click behavior (opening the edit
* modal) with your own handler.
*/
onDblClick?: (data: {
e: MouseEvent | null
calendarEvent: CalendarEvent
}) => void
/**
* Replaces the default cell-click behavior (opening the new-event
* modal in edit mode) with your own handler.
*/
onCellClick?: (data: CalendarCellClickData) => void
}
export interface CalendarActions {
createNewEvent: (event: CalendarEvent) => void
updateEventState: (event: CalendarEvent) => void
deleteEvent: (eventID: CalendarEvent['id']) => void
handleCellClick: (
e: MouseEvent,
date: Date | string,
time?: string,
isFullDay?: boolean,
) => void
updateActiveView: (
value: CalendarMode,
date?: Date,
isPreviousMonth?: boolean,
isNextMonth?: boolean,
) => void
/** Moves the calendar to a date, or to today when none is given. */
setCalendarDate: (date?: Date | string) => void
props: CalendarPublicProps
}
export type GroupedCalendarEvents = Record<string, CalendarEvent[]>
export const ACTIVE_VIEW_KEY = Symbol(
'frappe-ui.calendar.active-view',
) as InjectionKey<Ref<CalendarMode>>
export const CALENDAR_CONFIG_KEY = Symbol(
'frappe-ui.calendar.config',
) as InjectionKey<CalendarConfig>
export const CALENDAR_ACTIONS_KEY = Symbol(
'frappe-ui.calendar.actions',
) as InjectionKey<CalendarActions>Events to render. Each needs an `id`, a title, and date/time fields.
Behavior overrides, merged over the defaults.
Replaces the default single-click behavior (opening the event popover) with your own handler.
Replaces the default double-click behavior (opening the edit modal) with your own handler.
Replaces the default cell-click behavior (opening the new-event modal in edit mode) with your own handler.
| Slot | Payload |
|---|---|
header | { currentMonthYear: string; currentYear: number; currentMonth: number; enabledModes: CalendarActionO |
event-popover-content | { calendarEvent: { [x: string]: unknown; id?: string | number | undefined; name?: string | number | |
daily-header | { parseDateWithDay: any; currentDate: any; fullDay: any; } |
| Event | Payload |
|---|---|
delete | [eventID: string | number | undefined] |
create | [event: CalendarEvent] |
update | [event: CalendarEvent] |
rangeChange | [payload: { view: CalendarMode; startDate: string; endDate: string; }] |