Skip to content

FullCalendar — the cross-framework calendar & scheduler ​

FullCalendar is Rozie's data-bound port of FullCalendar — the vanilla-JS calendar/scheduler engine. FullCalendar already publishes four official wrappers (@fullcalendar/react, @fullcalendar/vue3, @fullcalendar/angular, @fullcalendar/svelte) — each one wraps the same Calendar engine, and Solid and Lit have none. Rozie ships the same <FullCalendar> (same props, events, two-way view binding, and imperative handle) in React, Vue, Svelte, Angular, Solid, and Lit.

This page is the show-and-tell: the API surface, per-framework quick starts, the imperative handle, the :options long-tail passthrough, the opt-in plugin-extension model, and the per-target recipe for the ten custom-content portal slots.

The full source for FullCalendar.rozie lives in the @rozie-ui/fullcalendar package.

The @rozie-ui/fullcalendar packages ​

FullCalendar ships as six pre-compiled, per-framework packages; install only the one for your framework. There is no build step and no Rozie toolchain to add:

PackageInstallREADME
@rozie-ui/fullcalendar-reactnpm i @rozie-ui/fullcalendar-reactreact/README
@rozie-ui/fullcalendar-vuenpm i @rozie-ui/fullcalendar-vuevue/README
@rozie-ui/fullcalendar-sveltenpm i @rozie-ui/fullcalendar-sveltesvelte/README
@rozie-ui/fullcalendar-angularnpm i @rozie-ui/fullcalendar-angularangular/README
@rozie-ui/fullcalendar-solidnpm i @rozie-ui/fullcalendar-solidsolid/README
@rozie-ui/fullcalendar-litnpm i @rozie-ui/fullcalendar-litlit/README

Each package carries the four @fullcalendar/* engine peers — @fullcalendar/core, @fullcalendar/daygrid, @fullcalendar/timegrid, and @fullcalendar/interaction (all ^6.1) — plus its framework peer (react + react-dom, vue, svelte, @angular/core + @angular/common, solid-js, or lit). Install the engine peers alongside the framework package:

bash
npm i @rozie-ui/fullcalendar-react \
  @fullcalendar/core @fullcalendar/daygrid @fullcalendar/timegrid @fullcalendar/interaction

No manual stylesheet import is needed. FullCalendar v6 auto-injects its own CSS at runtime — there is no import 'fullcalendar/...css' line to add (unlike the date-picker port, which requires a vendor stylesheet import). The wrapper's own <style> block carries only its layout box; the global .fc-* calendar styling comes from the engine itself.

Quick start ​

The two-way value is view — the active view name string ('dayGridMonth', 'timeGridWeek', 'timeGridDay', …). Clicking the calendar's own toolbar writes the new view name back through the two-way path, and a consumer write switches the calendar's view. Events are passed via :events; @eventClick surfaces the structured payload.

React ​

tsx
import { useState } from 'react';
import { FullCalendar } from '@rozie-ui/fullcalendar-react';

export function Demo() {
  const [view, setView] = useState('dayGridMonth');
  const [events] = useState([{ id: '1', title: 'Kickoff', start: '2026-06-04' }]);
  return (
    <FullCalendar
      view={view}
      onViewChange={setView}
      events={events}
      onEventClick={(e) => console.log(e.event, e.jsEvent)}
    />
  );
}

Vue ​

vue
<script setup lang="ts">
import { ref } from 'vue';
import FullCalendar from '@rozie-ui/fullcalendar-vue';

const view = ref('dayGridMonth');
const events = ref([{ id: '1', title: 'Kickoff', start: '2026-06-04' }]);
</script>

<template>
  <FullCalendar v-model:view="view" :events="events" @eventClick="(e) => console.log(e.event)" />
</template>

Svelte ​

svelte
<script lang="ts">
  import FullCalendar from '@rozie-ui/fullcalendar-svelte';

  let view = $state('dayGridMonth');
  let events = $state([{ id: '1', title: 'Kickoff', start: '2026-06-04' }]);
</script>

<FullCalendar bind:view {events} oneventclick={(e) => console.log(e.event)} />

Angular ​

ts
import { Component } from '@angular/core';
import { FullCalendar } from '@rozie-ui/fullcalendar-angular';

@Component({
  selector: 'app-demo',
  standalone: true,
  imports: [FullCalendar],
  template: `<FullCalendar [(view)]="view" [events]="events" (eventClick)="onEventClick($event)" />`,
})
export class DemoComponent {
  view = 'dayGridMonth';
  events = [{ id: '1', title: 'Kickoff', start: '2026-06-04' }];
  onEventClick(e: { event: unknown; jsEvent: unknown }) {
    console.log(e.event);
  }
}

Solid ​

tsx
import { createSignal } from 'solid-js';
import { FullCalendar } from '@rozie-ui/fullcalendar-solid';

export function Demo() {
  const [view, setView] = createSignal('dayGridMonth');
  const [events] = createSignal([{ id: '1', title: 'Kickoff', start: '2026-06-04' }]);
  return (
    <FullCalendar
      view={view()}
      onViewChange={setView}
      events={events()}
      onEventClick={(e) => console.log(e.event)}
    />
  );
}

Lit ​

ts
import '@rozie-ui/fullcalendar-lit';

// <rozie-full-calendar> is a custom element. Bind `view`/`events` as
// properties and listen for the `view-change` / `event-click` events.
const el = document.querySelector('rozie-full-calendar');
el.view = 'dayGridMonth';
el.events = [{ id: '1', title: 'Kickoff', start: '2026-06-04' }];
el.addEventListener('view-change', (e) => { el.view = e.detail; });
el.addEventListener('event-click', (e) => {
  console.log(e.detail.event, e.detail.jsEvent);
});

API ​

Props ​

NameTypeDefaultTwo-way (model)Description
eventsArray[]The event objects rendered on the calendar. Each event is normalized: a missing title renders as an empty title (the wrapper never invents one from the event id; an untitled event gets an aria-label so it still has an accessible name), and a missing color inherits defaultColor. Runtime-updatable — changing the array replaces only the events this prop supplied; events from options.eventSources or the addEvent verb are kept.
viewString"dayGridMonth"✓The two-way active view name ('dayGridMonth', 'timeGridWeek', 'timeGridDay', …). The calendar's own toolbar writes back through the two-way path; a consumer write calls changeView.
weekendsBooleantrueShow Saturday/Sunday columns. Runtime-updatable via setOption.
editableBooleantrueAllow events to be dragged and resized. Runtime-updatable.
selectableBooleantrueAllow date/time-range selection by click-drag. Runtime-updatable.
heightString | Number480The calendar height: a pixel number (480) or any CSS height FullCalendar accepts ('auto', '100%', '32rem', …). A purely numeric string ('600', e.g. from a static attribute) is treated as pixels. An empty string, null, or a number that is not positive falls back to the default 480. This curated prop wins over options.height at mount and after it (:options never applies a curated key), so size the calendar through height itself. Runtime-updatable.
defaultColorString"#3b82f6"Fallback event color stamped onto events that omit their own color.
localeString"en"FullCalendar locale code. Runtime-updatable. An object locale is an untyped runtime escape hatch — pass it through setOption via the handle if needed.
firstDayNumbernullFirst day of the week (0 = Sunday … 1 = Monday). Leave it unset (null, the default) to use the locale's first day, e.g. Monday for de. Runtime-updatable; setting it back to null after mount keeps the last applied day until remount.
slotDurationString"00:30:00"Time-grid slot length in HH:mm:ss. Runtime-updatable.
nowIndicatorBooleanfalseRender the current-time indicator line in time-grid views. Runtime-updatable.
headerToolbarObject{…}The toolbar layout ({ left, center, right }). A consumer-passed object fully replaces the built-in default. Runtime-updatable.
optionsObject{}Long-tail passthrough — an arbitrary bag of FullCalendar options/callbacks the curated surface doesn't special-case (businessHours, dayMaxEvents, *DidMount hooks, locale objects, …). Curated keys (the props above, events, every wrapped callback and filled *Content slot) always win: :options never overrides them, at mount or later, and only fills gaps. Runtime-updatable per key via setOption, and only for keys whose value actually changed — plain arrays and objects compare by content, so an inline literal re-created on every parent render (an inline eventSources list, say) does not refetch; functions compare by identity. A removed key keeps its last applied value until remount; use getApi() for full imperative control.

Events ​

EventDescription
eventClickAn event was clicked, or activated with Enter/Space. Payload: { event: { id, title, start, end }, jsEvent, el } — jsEvent is a MouseEvent, or a KeyboardEvent for keyboard activation. el is the clicked event's DOM element (use it as the anchor for a popover or tooltip).
dateClickA date/cell was clicked. Payload: { date, dateStr, allDay, dayEl, jsEvent } — dayEl is the clicked day cell (an anchor for a popover).
eventDropAn event was dragged to a new date. Payload: { event: { id, title, start, end }, oldEvent, delta, revert } — call revert() to reject the move.
selectA date/time range was selected. Payload: { start, end, startStr, endStr, allDay }.
eventResizeAn event was resized. Payload: { event: { id, title, start, end }, oldEvent, startDelta, endDelta, revert } — call revert() to reject the resize.
datesSetThe visible date range changed (navigation or view switch). Payload: { start, end, view }.
eventMouseEnterThe pointer entered a calendar event. Payload: { event: { id, title, start, end }, jsEvent, el } (mirrors eventClick).
eventMouseLeaveThe pointer left a calendar event. Payload: { event: { id, title, start, end }, jsEvent, el } (mirrors eventMouseEnter).
unselectA previously selected date/time range was cleared. Payload: { jsEvent } — the pointer/touch UIEvent that cleared it, or null when the selection was cleared programmatically (e.g. clearSelection()).
loadingThe calendar began or finished loading events (e.g. from an event source). Payload: { isLoading } boolean. It fires only while FullCalendar fetches an event source itself; with only the events array bound it never fires — see loading fires only for fetched sources.
eventsSetThe set of rendered events changed. Payload: { events: [{ id, title, start, end }, …] } — the normalized current event set, for persistence/sync consumers.

Imperative handle ​

Beyond props/events, the component exposes imperative methods declared once in the Rozie source via $expose. Grab a handle with your framework's native ref mechanism (React useRef / Vue template ref / Svelte bind:this / Angular viewChild / Solid callback ref / the Lit custom element itself) and call them directly:

MethodDescription
getApiReturn the underlying FullCalendar Calendar instance for direct API access. Returns the raw instance (null before mount).
changeViewSwitch the active view — changeView(viewName, dateOrRange?).
addEventAdd an event — addEvent(eventInput, source?).
removeEventRemove an event by id — removeEvent(id).
todayNavigate to today.
prevNavigate to the previous date range.
nextNavigate to the next date range.
gotoDateNavigate to a specific date — gotoDate(date).
getDateReturn the calendar's current anchor date (null before mount). The view model carries only the view type and datesSet only the visible range; this reads the current navigation date on demand.
getEventsReturn the current event objects synchronously ([] before mount). The pull-based twin of the push-only eventsSet event.
scrollToTimeScroll a time-grid view to a given time: scrollToTime(duration).
updateSizeRecompute the calendar layout after its container resizes.
prevYearNavigate to the previous year (mirrors prev).
nextYearNavigate to the next year (mirrors next).
selectRangeSelect a date/time range: selectRange(start, end?) (the Calendar API's select). Named selectRange because select is an emitted event.
clearSelectionClear the current date/time selection (the Calendar API's unselect). Named clearSelection because unselect is an emitted event.

React example:

tsx
import { useRef } from 'react';
import { FullCalendar, type FullCalendarHandle } from '@rozie-ui/fullcalendar-react';

const cal = useRef<FullCalendarHandle>(null);
// <FullCalendar ref={cal} ... />
cal.current?.next();
const api = cal.current?.getApi();

The selection verbs are named selectRange / clearSelection rather than bare select / unselect: this component emits select and unselect events, and an exposed method may not share a name with an emitted event (the same collision rule behind Flatpickr's openPicker / closePicker). Every other verb name is clear of the event and prop names as-is.

Slots ​

The wrapper surfaces ten of FullCalendar's *Content render hooks as portal slots — one authoring surface each that the per-target compiler routes through the framework's native imperative-render API (React/Solid render prop, Vue scoped slot, Svelte snippet, Angular content-child <ng-template>, Lit property bridge). Each slot is guarded in the wrapper: fill it and your fragment renders; leave it unfilled and FullCalendar's default rendering stands. Every slot receives one scope param, arg — FullCalendar's render argument for that hook.

SlotFullCalendar optionRendersDemo-verified
eventeventContentPer-event cell content (arg.event.title, arg.event.start, …)✓
dayCelldayCellContentDay-grid cell content (arg.date, arg.dayNumberText, …)✓
dayHeaderdayHeaderContentColumn-header content (arg.text, arg.date, …)✓
slotLabelslotLabelContentTime-grid axis slot labels (arg.text, arg.date, …)
weekNumberweekNumberContentWeek-number cell content (arg.num, arg.text, …)
nowIndicatorContentnowIndicatorContentCurrent-time indicator content (arg.isAxis, arg.date, …)
moreLinkmoreLinkContent"+N more" link content (arg.num, arg.text, …)
allDayContentallDayContentTime-grid all-day axis label content (arg.text, …)
slotLaneContentslotLaneContentTime-grid time-slot lane content (arg.date, arg.time, …)
noEventsContentnoEventsContentList-view "no events to display" content (arg.text, …) — inert until you engage @fullcalendar/list via :options.plugins and show an empty list view✓

All ten share the identical per-target authoring shape shown below — the only thing that changes is the slot name and the arg payload (per FullCalendar's hook for that surface). The three pixel-baselined slots (event, dayCell, dayHeader) are wired into the VR matrix; the seven long-tail slots (slotLabel, weekNumber, nowIndicatorContent, moreLink, allDayContent, slotLaneContent, noEventsContent) use the same recipe with no extra ceremony. The all-slots behavioral spec mounts all ten — including noEventsContent via a consumer-supplied list plugin, which transitively proves the plugin merge.

The current-time-indicator slot is named nowIndicatorContent (after FullCalendar's nowIndicatorContent option) so it does not clash with the boolean nowIndicator prop — a slot whose name equals a declared prop name is a hard compile error in Rozie (Svelte 5 unifies snippets and props into one $props namespace). Fill #nowIndicatorContent to customize the indicator content, and set the nowIndicator prop to true to actually enable it.

Custom event content ​

FullCalendar's eventContent option lets you replace the default per-cell title text with your own markup. The wrapper surfaces it as the event portal slot — a single authoring surface that the per-target compiler routes through each framework's native imperative-render API. Consumers fill it the same way they fill any other named slot. The slot receives one scope param, arg (FullCalendar's event-render argument; arg.event.title, arg.event.start, etc.).

Portal slots unlock the "foreign-engine cell rendering" pattern: FullCalendar owns the cell <div>, but the consumer's framework-native fragment is mounted inside it and disposed when the cell is torn down. See the portal-slot primitive for the underlying mechanism. (Note: portal slots are not reactive after mount in v1 — FullCalendar re-invokes eventContent when the event data changes, which is how the engine works anyway.)

React (render prop):

tsx
<FullCalendar
  view={view}
  events={events}
  renderEvent={({ arg }) => <span className="fc-event-title">{arg.event.title}</span>}
/>

Solid (render prop):

tsx
<FullCalendar
  view={view()}
  events={events()}
  eventSlot={({ arg }) => <span class="fc-event-title">{arg.event.title}</span>}
/>

Vue (scoped slot):

vue
<FullCalendar v-model:view="view" :events="events">
  <template #event="{ arg }">
    <span class="fc-event-title">{{ arg.event.title }}</span>
  </template>
</FullCalendar>

Svelte (snippet):

svelte
<FullCalendar bind:view {events}>
  {#snippet event({ arg })}
    <span class="fc-event-title">{arg.event.title}</span>
  {/snippet}
</FullCalendar>

Angular (content child <ng-template>):

html
<FullCalendar [(view)]="view" [events]="events">
  <ng-template #event let-arg="arg">
    <span class="fc-event-title">{{ arg.event.title }}</span>
  </ng-template>
</FullCalendar>

Lit (slot bridge — pass the render callback as a property):

ts
const el = document.querySelector('rozie-full-calendar');
el.event = ({ arg }) => html`<span class="fc-event-title">${arg.event.title}</span>`;

On every target the wrapper's $portals.event(node, { arg }) closure mounts the consumer's fragment into the engine-owned cell container and returns a dispose handle the engine calls on cell teardown.

Custom day-cell content ​

The dayCell portal slot (FullCalendar's dayCellContent) replaces a day-grid cell's default content. Same authoring shape as event — only the slot name and arg payload differ (arg.date, arg.dayNumberText, …):

React / Solid (render prop — renderDayCell on React, dayCellSlot on Solid):

tsx
// React
<FullCalendar view={view} events={events}
  renderDayCell={({ arg }) => <span className="my-day">{arg.dayNumberText}</span>} />

// Solid
<FullCalendar view={view()} events={events()}
  dayCellSlot={({ arg }) => <span class="my-day">{arg.dayNumberText}</span>} />

Vue (scoped slot):

vue
<FullCalendar v-model:view="view" :events="events">
  <template #dayCell="{ arg }">
    <span class="my-day">{{ arg.dayNumberText }}</span>
  </template>
</FullCalendar>

Svelte (snippet):

svelte
<FullCalendar bind:view {events}>
  {#snippet dayCell({ arg })}
    <span class="my-day">{arg.dayNumberText}</span>
  {/snippet}
</FullCalendar>

Angular (content child <ng-template>):

html
<FullCalendar [(view)]="view" [events]="events">
  <ng-template #dayCell let-arg="arg">
    <span class="my-day">{{ arg.dayNumberText }}</span>
  </ng-template>
</FullCalendar>

Lit (property bridge):

ts
el.dayCell = ({ arg }) => html`<span class="my-day">${arg.dayNumberText}</span>`;

Custom day-header content ​

The dayHeader portal slot (FullCalendar's dayHeaderContent) replaces a column header's default text. Identical recipe — substitute dayHeader for the slot name and read arg.text / arg.date:

vue
<!-- Vue -->
<FullCalendar v-model:view="view" :events="events">
  <template #dayHeader="{ arg }">
    <strong class="my-header">{{ arg.text }}</strong>
  </template>
</FullCalendar>
tsx
// React
<FullCalendar view={view} events={events}
  renderDayHeader={({ arg }) => <strong className="my-header">{arg.text}</strong>} />

The Solid (dayHeaderSlot={…}), Svelte ({#snippet dayHeader(…)}), Angular (<ng-template #dayHeader>), and Lit (el.dayHeader = …) forms follow the exact shapes shown for event/dayCell above.

The long-tail slots ​

slotLabel, weekNumber, nowIndicatorContent, moreLink, allDayContent, slotLaneContent, and noEventsContent use the same shared recipe — fill the like-named slot and read its arg. Each maps one-to-one to a FullCalendar *Content option:

SlotOptionTypical arg fields
slotLabelslotLabelContentarg.text, arg.date
weekNumberweekNumberContentarg.num, arg.text
nowIndicatorContentnowIndicatorContentarg.isAxis, arg.date
moreLinkmoreLinkContentarg.num, arg.text
allDayContentallDayContentarg.text
slotLaneContentslotLaneContentarg.date, arg.time
noEventsContentnoEventsContentarg.text

allDayContent and slotLaneContent render in time-grid views (timeGridWeek / timeGridDay) — the all-day axis label and the per-slot lane content respectively. Both work with the bundled @fullcalendar/timegrid plugin, no extra peer required. noEventsContent is pre-declared but inert with the bundled plugin set: it surfaces only in a list view (listWeek / listDay / listMonth) showing zero events, and list views exist only once you add the free @fullcalendar/list plugin — see Adding plugins.

For example, a custom week-number badge in Vue:

vue
<FullCalendar v-model:view="view" :events="events">
  <template #weekNumber="{ arg }">
    <span class="wk">W{{ arg.num }}</span>
  </template>
</FullCalendar>

Adding plugins ​

The wrapper ships batteries-included, zero-config: three FullCalendar plugins are baked in and always on — @fullcalendar/daygrid, @fullcalendar/timegrid, and @fullcalendar/interaction. Those cover month/week/day grids plus drag-select-resize out of the box, so the common case needs no plugin wiring at all.

When you need more, the wrapper is consumer-extensible: import any FullCalendar plugin and pass it through the :options.plugins passthrough. The wrapper merges your plugins with the baked-in defaults rather than replacing them (FullCalendar dedupes by identity, so re-passing a default is harmless). There is no per-plugin wrapper code and no bundle cost for plugins you don't engage — the extra plugin lives in your dependency tree, not the wrapper's.

The worked example is the list view + its noEventsContent slot. Add the free @fullcalendar/list plugin, pass it via :options.plugins, switch to a listWeek/listDay/listMonth view, and the noEventsContent portal slot becomes live (it renders when the list is empty):

bash
npm i @fullcalendar/list
vue
<script setup lang="ts">
import { ref } from 'vue';
import FullCalendar from '@rozie-ui/fullcalendar-vue';
import listPlugin from '@fullcalendar/list';

const view = ref('listWeek');
const events = ref([]); // empty → noEventsContent fires
</script>

<template>
  <FullCalendar
    v-model:view="view"
    :events="events"
    :options="{ plugins: [listPlugin] }"
  >
    <template #noEventsContent="{ arg }">
      <span class="empty">Nothing scheduled — enjoy your day.</span>
    </template>
  </FullCalendar>
</template>

The same opt-in mechanism engages every other FullCalendar plugin uniformly across all six targets — @fullcalendar/rrule (recurring events; recurring fields like rrule already pass through :events untouched), @fullcalendar/google-calendar, @fullcalendar/luxon3 / time-zone plugins, and the bootstrap/theme plugins. The premium scheduler plugins (@fullcalendar/resource-timeline, @fullcalendar/resource-timegrid, …) work the same way — pass the plugin via :options.plugins and additionally supply your schedulerLicenseKey through :options (:options="{ plugins: [resourceTimelinePlugin], schedulerLicenseKey: '…' }").

If you'd rather drive a one-off option imperatively, the :options passthrough also forwards arbitrary non-plugin options, and getApi() returns the raw Calendar instance for full control.

Recipes ​

Two-way view binding ​

The view prop is the only two-way value. Both the calendar's own toolbar buttons (dayGridMonth / timeGridWeek / timeGridDay) AND a consumer write update the bound state, with a round-trip guard so a programmatic changeView does not echo back:

vue
<script setup lang="ts">
import { ref } from 'vue';
import FullCalendar from '@rozie-ui/fullcalendar-vue';
const view = ref('dayGridMonth');
</script>

<template>
  <button @click="view = 'timeGridWeek'">Week</button>
  <FullCalendar v-model:view="view" :events="events" />
  <p>Current view: {{ view }}</p>
</template>

Reconciling events at runtime ​

Changing the :events array reconciles the live calendar without remounting — the wrapper runs FullCalendar's supported removeAllEvents + addEvent loop, normalizing each event on the way in (a missing color inherits defaultColor; a missing title stays empty — the wrapper never invents one from the event id). Just bind a reactive array and push/replace it:

vue
<FullCalendar :events="events" v-model:view="view" />
<!-- events.value = [...events.value, { id: 'new', title: 'Sync', start: '2026-06-10' }] -->

weekends, editable, selectable, height, locale, firstDay, slotDuration, nowIndicator, and headerToolbar are likewise runtime-updatable — each is wired to FullCalendar's setOption path, so changing the prop reconciles the live calendar with no re-key.

Driving navigation from the handle ​

The imperative verbs cover the navigation/mutation surface that props alone can't express. Grab the handle and call next()/prev()/today()/prevYear()/nextYear()/gotoDate()/changeView(), or reach the raw engine via getApi():

tsx
const cal = useRef<FullCalendarHandle>(null);
// <FullCalendar ref={cal} ... />
<button onClick={() => cal.current?.next()}>Next</button>
<button onClick={() => cal.current?.today()}>Today</button>

Gotchas ​

Round-trip-guarded view sync ​

The two-way view prop is guarded against the cross-framework "infinite update loop" bug class: a programmatic changeView sets a suppressViewSync flag so the engine's viewDidMount/datesSet callback does not write the same value back into $model.view. A user toolbar click flows up normally.

getApi returns the raw instance ​

getApi() returns the underlying Calendar instance directly (it is not guard-nulled away) so you can call any FullCalendar API the wrapper doesn't surface. It is null before mount — callers handle the pre-mount null.

Custom event content is not reactive after mount ​

Per the v1 portal-slot constraint, the event slot re-renders only when FullCalendar re-invokes eventContent (i.e. when the event data changes). This matches the engine's own behavior — it is not a limitation specific to the wrapper.

Events are keyboard-focusable ​

Whenever an eventClick handler is registered, FullCalendar makes every event interactive (tabindex="0", Enter/Space activation). The wrapper always registers one, because it powers the eventClick event. FullCalendar checks the eventInteractive option before it checks for handlers, so opt out through the passthrough: :options="{ eventInteractive: false }". A per-event interactive: false also works when the global option is unset.

loading fires only for fetched sources ​

loading reflects event sources FullCalendar fetches itself (a URL/JSON feed or a function source). The events prop is always an in-memory array reconciled synchronously, so with events alone loading never fires — track your own fetch state instead. For FullCalendar-managed fetching, pass sources through options as eventSources: :options="{ eventSources: [{ url: '/api/events' }] }".

noEventsContent needs a list view ​

The noEventsContent slot renders only in list views, and the baked-in daygrid/timegrid/interaction plugins provide none. Engage @fullcalendar/list and set view to listWeek / listDay / listMonth — see the worked example in Adding plugins.

Cross-references ​

Pre-1.0 — APIs may change between minor versions.