Full calendar
Displays dates like a planner in day, week, month, year and agenda views, with events, multi-day bars, a sticky header while the hours scroll and a +N more panel.
Component
October 2026
Props
| Props | Default | Type |
|---|---|---|
modelValue | null | string | null |
view | FullCalendarView.MONTH | FullCalendarView |
views | Object.values(FullCalendarView) | FullCalendarView[] |
events | [] | FullCalendarEvent[] |
maxEvents | 3 | number |
agendaDays | 30 | number |
timeGridMaxHeight | '40rem' | string |
timeGridHeaderClass | 'bg-background-container-surface' | string |
hourFormat | HourFormat.HOURS_24 | HourFormat |
locale | 'en-US' | string |
firstDayOfWeek | 0 | number |
color | ColorAccent.PRIMARY_BRAND | ColorAccent |
getEventColor | — | (event: FullCalendarEvent) => ColorAccent | undefined |
morePopoverAutoFlip | true | boolean |
hasEventPopover | true | boolean |
isAddable | false | boolean |
isEditable | false | boolean |
isDeletable | false | boolean |
headingTag | 'h2' | HeadingTag |
isBoxed | true | boolean |
disabled | false | boolean |
ariaLabel | — | string |
previousMonthAriaLabel | — | string |
nextMonthAriaLabel | — | string |
Slots
| Name | Description |
|---|---|
event | Content of an event in every view, also in the `+N more` panel. Receives `event` (the `FullCalendarEvent`). It replaces what is inside the event: the dot, the time and the title in the month and the agenda, and the title and time of the block in the day and week. |
more-popover | Content of the `+N more` panel. Receives `date` (the ISO date), `label` (the full date as text), `events` (all the events of the day) and `close` (a function that closes the panel). It replaces the title, the close button and the list, and the panel keeps its box and its placement. |
event-popover | Content of the popover of an event. Receives `event` (the `FullCalendarEvent`), `close` (a function that closes the popover), `edit` (a function that emits `event-edit` with the event and closes the popover) and `remove` (a function that emits `event-delete` with the event and closes the popover). It replaces the information and the buttons, and the popover keeps its box and its placement. |
Usage
All dates are ISO strings (YYYY-MM-DD), the same format the native date input uses. No time zone is involved.
modelValue
Sets the selected day, shown with a soft highlight. You can bind it with v-model. Today is always shown with a filled circle.
<template>
<FullCalendar v-model="date" :events />
</template>
<script setup lang="ts">
const date = ref<string | null>('2026-10-12')
</script>
view
Sets the view that is shown via the FullCalendarView enum. You can bind it with v-model:view. The view menu and the keyboard shortcuts change it too, and every view shows the same focused day, so changing the view keeps the date.
<template>
<FullCalendar v-model:view="view" :events />
</template>
<script setup lang="ts">
const view = ref(FullCalendarView.WEEK)
</script>
In the DAY and WEEK views the header, with the dates and the events of the whole day, stays on top while the hours scroll. The height of the hours is set with timeGridMaxHeight.
Options
| Value | Description |
|---|---|
DAY | day: one day with its hours. The events with a time are blocks on the hours, and the rest are in the all day row. Events that share hours are put side by side. |
WEEK | week: the 7 days of the week with their hours. Clicking a day opens it in the day view. |
MONTH | month: the weeks of the month, with the events laid over the days. |
YEAR | year: the 12 months. Clicking a day opens it in the day view, and clicking a month opens it in the month view. |
AGENDA | agenda: the list of the days that have events, starting on the shown day (see [agendaDays](#agendadays)). |
views
Sets the views that can be chosen, shown in the order of the enum. The menu is hidden when there is only one view. A view that is not in the list also ignores its shortcut and is skipped when a day or a month is clicked.
<template>
<FullCalendar :views="[FullCalendarView.MONTH, FullCalendarView.WEEK]" />
</template>
events
Sets the events to show. An event that spans several days, or that has no valid time (HH:mm), is a colored bar: in the month and the agenda it is on its days, and in the day and week it is in the all day row. A single-day event with a time is a dot followed by the time and the title in the month, and a block from time to endTime in the day and week (an hour long without endTime). Blocks that share hours are put side by side. A bar that crosses weeks continues on the next row. Events that already ended are shown faded in every view, but they can still be clicked: an event of the day is past after its last day, and an event with a time after its endTime (an hour after time without it). Events with an invalid start are ignored.
<template>
<FullCalendar :events />
</template>
<script setup lang="ts">
const events: FullCalendarEvent[] = [
{ id: '1', title: 'Product launch', start: '2026-10-12', color: ColorAccent.DANGER },
{ id: '2', title: 'Team offsite', start: '2026-10-14', end: '2026-10-16', color: ColorAccent.SUCCESS },
{ id: '3', title: 'Client demo', start: '2026-10-15', time: '16:00' },
]
</script>
TypeScript Interface
export interface FullCalendarEvent {
id?: string
title: string
start: string
end?: string // The last day, inclusive
time?: string // HH:mm
endTime?: string // HH:mm
color?: ColorAccent
meta?: Record<string, unknown> // Anything else you need back
}
The meta field is for your own data, such as a category or the id of the record in your API. It is not read by the calendar: it comes back untouched in event-click, more-click, the event slot and getEventColor.
<template>
<FullCalendar :events @event-click="openEvent" />
</template>
<script setup lang="ts">
const events: FullCalendarEvent[] = [
{ title: 'Design review', start: '2026-10-12', meta: { category: 'work', recordId: 42 } },
]
const openEvent = (event: FullCalendarEvent) => router.push(`/records/${event.meta?.recordId}`)
</script>
maxEvents
Sets how many events a day shows. The rest are collapsed into a +N more button. Clicking it opens a panel over the day with all of its events, and emits more-click.
<template>
<FullCalendar :events :maxEvents="2" />
</template>
agendaDays
Sets how many days the AGENDA view covers, from the focused day. The arrows move by this amount. Only the days that have events are listed.
<template>
<FullCalendar :view="FullCalendarView.AGENDA" :agendaDays="14" />
</template>
timeGridMaxHeight
Sets the maximum height of the hours of the DAY and WEEK views, as a CSS length. The hours scroll inside it, and the header with the dates stays on top. The view starts an hour before the current time, or at 7:00 when today is not shown.
<template>
<FullCalendar :view="FullCalendarView.WEEK" timeGridMaxHeight="30rem" />
</template>
timeGridHeaderClass
Sets the Tailwind classes of the sticky header of the DAY and WEEK views, the one with the dates and the all day events that stays on top while the hours scroll. They replace the default bg-background-container-surface, so include a background when you set it. Use theme tokens, such as bg-background-neutral-subtlest, to keep the dark mode working.
<template>
<FullCalendar :view="FullCalendarView.WEEK" timeGridHeaderClass="bg-background-neutral-subtlest" />
</template>
hourFormat
Sets how the hours of the day and week views are written, via the HourFormat enum: 13:00 or 1:00 PM. The times of the events are shown as they are given.
<template>
<FullCalendar :view="FullCalendarView.WEEK" :hourFormat="HourFormat.HOURS_12" />
</template>
Options
| Value | Description |
|---|---|
HOURS_12 | 12: hours with AM / PM, such as `1:00 PM` |
HOURS_24 | 24: hours from 00 to 23, such as `13:00` |
locale
Sets the locale used for the month, the weekdays and the full date of each day.
<template>
<FullCalendar locale="es-ES" :firstDayOfWeek="1" />
</template>
firstDayOfWeek
Sets the first day of the week, from 0 (Sunday) to 6 (Saturday).
<template>
<FullCalendar :firstDayOfWeek="1" />
</template>
color
Sets the accent of today, the selected day and the focus ring via the ColorAccent enum. Events use their own color.
<template>
<FullCalendar :color="ColorAccent.SECONDARY_BRAND" />
</template>
Options
| Value | Description |
|---|---|
PRIMARY_BRAND | primary-brand |
SECONDARY_BRAND | secondary-brand |
getEventColor
Sets the color of each event with a function, so you can map your own data to the ColorAccent colors. It wins over the color of the event, and when it returns undefined the color of the event is used (primary brand by default). The colors are always the accent tokens of the theme, never raw colors.
<template>
<FullCalendar :events :getEventColor="getColor" />
</template>
<script setup lang="ts">
const colors: Record<string, ColorAccent> = {
work: ColorAccent.INFO,
personal: ColorAccent.SUCCESS,
deadline: ColorAccent.DANGER,
}
const getColor = (event: FullCalendarEvent) => colors[event.meta?.category as string]
</script>
morePopoverAutoFlip
Moves the +N more panel to the other side when it does not fit in the calendar: upwards in the last rows, and towards the left or the right when it goes out on a side. The panel is centered on its day in the middle columns, starts at the day in the first columns, and ends at it in the last ones. Set it to false to keep that initial placement without measuring. The popover of an event is also centered on the event and always kept inside the calendar.
<template>
<FullCalendar :events :morePopoverAutoFlip="false" />
</template>
hasEventPopover
Opens a popover with the information of an event when it is clicked: its title, its day or days, and its time (or "All day"). It works in every view, also in the +N more panel. It closes with its close button, with Escape, when you click outside, or when the view or the dates change. event-click is emitted either way, so set it to false to show your own dialog instead.
<template>
<FullCalendar :events :hasEventPopover="false" @event-click="openDialog" />
</template>
Use the event-popover slot to change what the popover shows.
isAddable
Lets you add events by clicking a cell. Clicking a day of the month emits event-add with { date }, and clicking an empty hour of the day or week views emits it with { date, time } (in steps of 30 minutes). The calendar does not create anything: open a form or a dialog in the handler, save the event and add it to events.
<template>
<FullCalendar :events isAddable @event-add="openForm" />
</template>
<script setup lang="ts">
const openForm = ({ date, time }: FullCalendarAddEvent) => console.log(date, time)
</script>
TypeScript Interface
export interface FullCalendarAddEvent {
date: string
time?: string // HH:mm, only in the day and week views
}
isEditable
Shows an edit button in the popover of an event. Clicking it closes the popover and emits event-edit with the event. The calendar does not change the event: open your form in the handler, save it (it can be asynchronous), and update events when it is done.
<template>
<FullCalendar :events isEditable @event-edit="openForm" />
</template>
<script setup lang="ts">
const openForm = (event: FullCalendarEvent) => console.log('Edit', event.id)
</script>
It needs hasEventPopover.
isDeletable
Shows a delete button in the popover of an event. Clicking it closes the popover and emits event-delete with the event. The calendar does not remove the event: delete it in the handler, which can be asynchronous, and update events when it is done.
<template>
<FullCalendar :events isDeletable @event-delete="removeEvent" />
</template>
<script setup lang="ts">
const events = ref<FullCalendarEvent[]>([])
const removeEvent = async (event: FullCalendarEvent) => {
await api.deleteEvent(event.id)
events.value = events.value.filter((item) => item.id !== event.id)
}
</script>
It needs hasEventPopover.
headingTag
Sets the HTML tag of the month title, so the heading level fits the outline of your page. It accepts h1 to h6.
<template>
<FullCalendar headingTag="h3" />
</template>
isBoxed
Shows the calendar inside a box, with a border, rounded corners and a surface background. Set it to false to place the calendar in a container that already has its own box.
<template>
<FullCalendar :isBoxed="false" />
</template>
disabled
Disables the navigation, the days and the events.
<template>
<FullCalendar :disabled="true" />
</template>
ariaLabel
Sets the accessible name of the calendar.
<template>
<FullCalendar ariaLabel="Team calendar" />
</template>
previousMonthAriaLabel
Overrides the aria-label of the previous month button. By default it comes from useDSConfig().calendar.
nextMonthAriaLabel
Overrides the aria-label of the next month button. By default it comes from useDSConfig().calendar.
Accessibility
The calendar is a group. The title of the month is announced when it changes. Each day is a button with its full date as aria-label, aria-pressed when it is selected and aria-current="date" when it is today. Each event is a button named after its time and title.
The month view is described below. In the day and week views the dates of the header are buttons, with their full date as aria-label, and the hours are a plain grid of events. The year view has a button for each month and each day, and the agenda lists each day with a button for its date and one for each event.
The view menu opens with the mouse or the keyboard, and each view has a shortcut when the focus is in the calendar (except while typing in a field, or with Ctrl, Cmd or Alt):
| Value | Description |
|---|---|
D | Shows the day view. |
W | Shows the week view. |
M | Shows the month view. |
Y | Shows the year view. |
A | Shows the agenda view. |
In the month view only one day is in the tab order. Once the focus is on a day, these keys move it:
| Value | Description |
|---|---|
ArrowLeft / ArrowRight | Moves one day backward or forward. |
ArrowUp / ArrowDown | Moves one week backward or forward. |
Home / End | Moves to the first or last day of the month. |
PageUp / PageDown | Moves one month backward or forward. |
Shift + PageUp / PageDown | Moves one year backward or forward. |
Enter / Space | Selects the focused day. |
The popover of an event is a dialog named after the event. Escape closes it and returns the focus to the event, and its buttons are named after useDSConfig().calendar.editEventText(), useDSConfig().calendar.deleteEventText() and useDSConfig().actions.closeText().
The +N more button has aria-haspopup="dialog" and aria-expanded. The panel is a dialog named after the date, and Escape closes it and returns the focus to the day. It also closes when you click outside, click a day, pick an event or change the month.
The texts of the buttons, the views and the agenda come from useDSConfig().calendar, and the close button text from useDSConfig().actions.
Emits
| Value | Description |
|---|---|
@day-click | Emitted with `{ date, key, isDisabled }` when a day is clicked in the month, or a day of the header, the year or the agenda is opened. |
@update:view | Emitted with the new `FullCalendarView` when the view changes, by the menu, a shortcut or a day or month that opens another view. |
@range-change | Emitted with `{ view, from, to }` when the view or the dates it shows change. `from` and `to` are the first and last day (ISO dates) that the view shows, so you can load its events. In the month they are the first and last day of the grid, including the days of the months around it. |
@slot-click | Emitted with `{ date, time }` when an empty part of a day in the day or week view is clicked. `time` is the start of the half hour (`HH:mm`), so you can open a form to create an event. |
@event-click | Emitted with the `FullCalendarEvent` when an event is clicked. The day is not selected. |
@event-add | Emitted with `{ date, time? }` when a cell is clicked and [isAddable](#isaddable) is set: a day of the month (`date` only) or an empty hour of the day and week views. The calendar does not add the event, so you can ask for its data or call your API first. |
@event-edit | Emitted with the `FullCalendarEvent` when the edit button of its popover is clicked and [isEditable](#iseditable) is set. The calendar does not change the event, so you can open a form, call your API and then update `events`. |
@event-delete | Emitted with the `FullCalendarEvent` when the delete button of its popover is clicked and [isDeletable](#isdeletable) is set. The calendar does not remove the event, so you can call your API first and then update `events`. |
@month-change | Emitted with `{ year, month, from, to }` when the visible month changes. `month` goes from 1 to 12, and `from` and `to` are the first and last day of the month as ISO dates, so you can load its events. |
@more-click | Emitted with `{ date, events }` when a `+N more` button is clicked, after the panel opens. `events` are all the events of the day. |
@update:modelValue | Emitted with the new ISO date when a day is selected. |
Example
<template>
<FullCalendar
v-model="date"
:events
isAddable
isEditable
isDeletable
@event-click="openEvent"
@event-add="addEvent"
@event-edit="editEvent"
@event-delete="deleteEvent"
@more-click="openDay"
@month-change="loadEvents"
/>
</template>
<script setup lang="ts">
const date = ref<string | null>(null)
const events = ref<FullCalendarEvent[]>([])
const openEvent = (event: FullCalendarEvent) => console.log('Event', event.title)
const openDay = ({ date, events }: FullCalendarMoreEvent) => console.log(date, events.length)
const addEvent = async ({ date, time }: FullCalendarAddEvent) => {
const created = await api.createEvent({ title: 'New event', start: date, time })
events.value = [...events.value, created]
}
const editEvent = (event: FullCalendarEvent) => console.log('Edit', event.id)
const deleteEvent = async (event: FullCalendarEvent) => {
await api.deleteEvent(event.id)
events.value = events.value.filter((item) => item.id !== event.id)
}
const loadEvents = async ({ from, to }: CalendarMonthEvent) => {
events.value = await fetchEvents(from, to)
}
</script>
Custom panel
<template>
<FullCalendar :events>
<template #more-popover="{ label, events, close }">
<p class="font-semibold">{{ label }}</p>
<ul>
<li v-for="event in events" :key="event.id">{{ event.title }}</li>
</ul>
<ActionButton text="Done" @click="close" />
</template>
</FullCalendar>
</template>
TypeScript Interface
export interface FullCalendarMoreEvent {
date: string
events: FullCalendarEvent[]
}
export interface FullCalendarRangeEvent {
view: FullCalendarView
from: string
to: string
}
export interface FullCalendarSlotEvent {
date: string
time: string
}
Methods
| Value | Description |
|---|---|
focus() | Moves the focus to the selected day (or the last focused one) so keyboard navigation can start. |
<template>
<FullCalendar ref="calendar" v-model="date" />
<ActionButton text="Focus" @click="calendar?.focus()" />
</template>
<script setup lang="ts">
const calendar = ref()
</script>