Calendar ​

A date and event view for schedules, with Month, Week, Day, and Agenda modes.

Parked — Calendar left the root export in 1.0.0 and now ships from frappe-ui/experimental with its public API unchanged. It stays there, exempt from the deprecation policy, until a redesigned calendar family replaces it.

ts
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:

ts
{
  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 it is the same grid a size down: shorter pills, a tighter lane, and a cell that shows what fits in it with a +n count for the rest — a full week trades its last lane of bars for those counts. Clicking a date number at either width 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.

Agenda view ​

The Agenda view is three months as a list of days, each day a card: its name, its date, how much is on it once that is more than one thing, then its events as rows. A day with nothing on it is not listed — an empty row says nothing the dates either side of it do not, and neither does a line counting how many were skipped. Cards are grouped under the week they fall in, which replaces the month dividers a flat list needs. Today's card carries a dot before its name; it, the day before it and the day after say so in words beside their dates, and a day already spent fades its header. An event that has ended is dimmed, the way a pill in the grid is once its time has passed. An event under way says so with its own tag.

The window covers the month in view and the two after it, padded out to whole weeks at either end the way the Month view's strip is — the list groups its days under the week they fall in, and a week is either listed or it is not. A single month would be 31 days on the 1st and one day on the 31st, which is why it is three. The arrows step a month at a time, so each move keeps two thirds of what was on screen, and rangeChange reports exactly the span listed, padding included, so a data source fetching by range agrees with it. The header names the three months themselves, not the days the padding reaches into.

Rows have the room a grid pill does not, so they carry a description line and tags. Calendar fills in the description itself — where the event is, and which day of a stay the row is (Day 2/3) — and marks a draft as a pill does, an outline in its colour on the plain ground rather than a tinted block. Where it stands against the clock reads right after that: Now in blue while it runs and Soon in amber in the hour before it, and nothing beyond that: an hour count would only restate the time written beside it. Who is coming (participant) stands at the row's far end, the one edge a card aligns on other than the time column, so the counts of a day's rows line up — and the #event-description and #event-suffix slots let you say the rest.

Drawn narrower than 640px — a phone, or a pane a sidebar has squeezed — the list stacks. A day's name becomes a band over its rows rather than a column beside them, and each row takes two lines: the title, its tags and who is coming on the first, the time and the description on the second. The list measures its own width for this, so a calendar in a narrow pane reads the same way a phone's does.

Config ​

The config prop takes a partial CalendarConfig; unset keys use these defaults:

ts
{
  defaultMode: 'Month',   // 'Day' | 'Week' | 'Month' | 'Agenda'
  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 / a 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 }.

The Agenda's rows take three more slots: #event-description for the line under the title, #event-suffix for the tags beside it, and #event-participant for the row's far end, where the event's participant string is shown unless you fill it — with faces in front of the count, say. All three receive { calendarEvent, date, description, timing }, where description and timing are what the calendar derived itself, so you can add to them rather than work them out again. The suffix has no tags of its own to hand you — it is yours to fill. Grid pills have no room for any of these and ignore the slots.

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:

vue
<Calendar ref="calendar" :events="events" />
ts
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 picker

API Reference ​

Show types
typescript
import type { Component, InjectionKey, Ref } from 'vue'
import type { BadgeProps } from '#components/Badge/types'

export type CalendarMode = 'Day' | 'Week' | 'Month' | 'Agenda'
export type CalendarTimeFormat = '12h' | '24h'

/**
 * One event colour: the bar, the fill, the fill a step deeper for hover and
 * selection, and the muted ink inside. Selection never repaints the text, so
 * there is no active ink here to fall out of step with the fill it sits on.
 */
export interface CalendarColor {
  color: string
  border: string
  subtext: string
  bg: 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
  /** The events this pill is drawn on, with their own place in the layout — see `findOverlappingEventsCount`. */
  over?: CalendarEvent[]
  [key: string]: unknown
}

/**
 * A tag on a listed event — the Agenda's rows carry these where a grid pill has
 * no room for them. Rendered as a `Badge`, so `theme` is Badge's own and a
 * consumer's tags in `#event-suffix` sit beside the timing tag as equals.
 */
export interface CalendarRowTag {
  label: string
  theme?: BadgeProps['theme']
  /** Badge's own; `solid` for the one tag that has to be seen before it is read. */
  variant?: BadgeProps['variant']
}

/** What `#event-description`, `#event-suffix` and `#event-participant` receive. */
export interface CalendarRowSlotProps {
  calendarEvent: CalendarEvent
  /** The day the row belongs to; a multi-day event has one row per day. */
  date: Date
  /** The library's own description, so a filled slot extends rather than re-derives. */
  description: string
  /** Where the event stands against the clock, shown beside the time. */
  timing: CalendarRowTag | null
}

/**
 * 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[]

  /**
   * Whether the events for the visible range are still on their way.
   *
   * Only the Agenda reads it, and only to tell an empty list apart from one that
   * has not arrived: a grid with nothing in it still draws the days, where a list
   * with nothing in it is a blank panel, and saying "nothing on" of a range still
   * being fetched is saying something that may not be true.
   */
  loading?: boolean

  /** 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: {
    /** A key event when the row was activated from the keyboard. */
    e: MouseEvent | KeyboardEvent
    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*
= []
CalendarEvent[]

Events to render. Each needs an `id`, a title, and date/time fields.

loading
boolean

Whether the events for the visible range are still on their way. Only the Agenda reads it, and only to tell an empty list apart from one that has not arrived: a grid with nothing in it still draws the days, where a list with nothing in it is a blank panel, and saying "nothing on" of a range still being fetched is saying something that may not be true.

config
= {}
Partial<CalendarConfig>

Behavior overrides, merged over the defaults.

onClick
((data: { e: MouseEvent | KeyboardEvent; calendarEvent: CalendarEvent; }) => void)

Replaces the default single-click behavior (opening the event popover) with your own handler.

onDblClick
((data: { e: MouseEvent | null; calendarEvent: CalendarEvent; }) => void)

Replaces the default double-click behavior (opening the edit modal) with your own handler.

onCellClick
((data: CalendarCellClickData) => void)

Replaces the default cell-click behavior (opening the new-event modal in edit mode) with your own handler.

header
{ currentMonthYear: string; currentYear: number; currentMonth: number; enabledModes: CalendarActionO
event-popover-content
{ calendarEvent: CalendarEvent; date: Date; isEditMode: boolean; close: () => void; }
event-description
{ calendarEvent: CalendarEvent; date: Date; description: string; timing: CalendarRowTag | null; }
event-suffix
{ calendarEvent: CalendarEvent; date: Date; description: string; timing: CalendarRowTag | null; }
event-participant
{ calendarEvent: CalendarEvent; date: Date; description: string; timing: CalendarRowTag | null; }
delete
[eventID: string | number | undefined]
create
[event: CalendarEvent]
update
[event: CalendarEvent]
rangeChange
[payload: { view: CalendarMode; startDate: string; endDate: string; }]
reloadEvents
() => void

Rebuilds the calendar's own copy of `events`. The calendar already does this whenever `events` changes.

currentMonthYear
string

The title of the visible range, e.g. "August 2026".

currentYear
number

The year of the visible month.

currentMonth
number

The visible month, `0` for January.

currentDay
number | null

The day of the month the view is anchored on.

enabledModes
CalendarActionOption[]

The views that `config` does not disable. Read once, when the calendar mounts.

activeView
CalendarMode

The visible view.

decrement
() => void

Moves back one day, week or month, depending on the view.

increment
() => void

Moves forward one day, week or month, depending on the view.

updateActiveView
(value: CalendarMode, d?: Date, isPreviousMonth?: boolean, isNextMonth?: boolean) => void

Switches to this view.

setCalendarDate
(d?: string | Date) => void

Jumps to this date, or to today when no date is given.

onMonthYearChange
(val?: string | Date) => void

Jumps to this date and moves the month picker to it.

selectedMonthDate
string

The month picker's date, as `YYYY-MM-DD`.