Skip to content

@carrot/design-vue-calendar

Vue 3 components and composables for calendar views. Wraps @carrot/design-components-calendar CSS with behaviour, navigation, event positioning, drag/resize, recurring event support via @carrot/temporal, and popover for event details.

Zero CSS in this package. All visual styling comes from @carrot/design-components-calendar.

Installation

bash
npm install @carrot/design-vue-calendar

Requires the component-level CSS to be imported separately:

css
@import "@carrot/design-components-calendar";

Quick Start

vue
<script setup lang="ts">
import { ref } from "vue";
import { CarrotCalendar, type CalendarEventData } from "@carrot/design-vue-calendar";

const currentDate = ref("2026-03-22");
const events = ref<CalendarEventData[]>([
  { id: "1", title: "Standup", start: "2026-03-22T09:00Z", end: "2026-03-22T09:30Z", variant: "accent" },
]);
</script>

<template>
  <CarrotCalendar v-model="currentDate" :events="events" editable />
</template>

Components

CarrotCalendar (Root)

Props: modelValue · view · events · weekStart · slotInterval · showWeekNumbers · showAllDay · compact · timezone · editable · workingHoursStart · workingHoursEnd · workingHoursMode · weekDays · formatDate · formatTime Events: update:modelValue · update:view · event:click · event:dblclick · event:drop · event:resize · slot:click · date:click · more:click · series:edit · event:delete Slots: header Provides: CalendarContext via CALENDAR_INJECTION_KEY

CarrotCalendarHeader

Props: title · showToday · showViewSwitcher · availableViews Slots: nav-prev · nav-next · nav-today · title · view-switcher · actions

CarrotCalendarEvent

Props: event (required) · variant · size · selected · dragging · resizing · continued · continues Events: click · dblclick · dragstart Slots: default · time

CarrotCalendarMonthEvent

Props: event (required) · variant · continued · continues Events: click · dblclick Slots: default

CarrotCalendarEventPopover

Props: event · open · anchor · isRecurring Events: update:open · close · edit:this · edit:future · edit:all · delete:this · delete:future · delete:all Slots: header · default · actions

CarrotCalendarDayView / CarrotCalendarWeekView

Internal view components. Inject CalendarContext. Emit: event:click · event:dblclick · event:drop · event:resize · slot:click

CarrotCalendarMonthView

Internal view component. Emits: event:click · event:dblclick · date:click · more:click

CarrotCalendarYearView

Internal view component. Emits: date:click

CarrotCalendarNowIndicator

Rendered automatically in day/week views for today's column.


Composables

useCalendarNavigation(options?)

Date navigation state machine. Returns currentDate, view, rangeStart, rangeEnd, visibleDays, title, next(), prev(), today(), goTo(), setView().

Options: initialDate · view · weekStart · weekDays

useCalendarEvents(options)

Event filtering and overlap layout. Returns eventsForDate(), allDayEventsForDate(), timedEventsForDate(), positionEvents().

Options: events · slotHeight · slotInterval

useCalendarGrid(options?)

Time slot generation and coordinate conversion. In "hide" mode, slots outside working hours are excluded entirely. Returns timeSlots, hourLabels, slotHeight, dayHeight, visibleStartMinutes, minutesToPixels(), pixelsToMinutes(), groupIntoWeeks(), groupIntoMonths().

Options: slotInterval · workingHoursStart · workingHoursEnd · workingHoursMode

useCalendarDrag(options)

Pointer-based drag with snap. Returns state, beginDrag(), isDragging().

Options: minutesToPixels · pixelsToMinutes · snapMinutes · onDrop · onResize

useNowIndicator(options?)

Real-time clock. Returns nowMinutes, todayDate, isToday().

Options: interval · enabled


CalendarContext (Provide/Inject)

Provided by CarrotCalendar via CALENDAR_INJECTION_KEY.

State: view · currentDate · rangeStart · rangeEnd · visibleDays · slotInterval · slotHeight · weekStart · timezone · workingHoursStart · workingHoursEnd · workingHoursMode · visibleStartMinutes · editable · events · selectedEventId Navigation: next() · prev() · today() · goTo() · setView() Events: selectEvent() · eventsForDate() · allDayEventsForDate() · timedEventsForDate() Conversion: minutesToPixels() · pixelsToMinutes() · formatDate() · formatTime()


CalendarEventData

ts
interface CalendarEventData {
  id: string;
  title: string;
  description?: string;
  start: string;
  end: string;
  allDay?: boolean;
  variant?: CalendarEventVariant;
  color?: string;
  colorFg?: string;
  icon?: string;
  location?: string;
  editable?: boolean;
  recurrence?: RecurringEvent;
  seriesId?: string;
  instanceIndex?: number;
  isOverride?: boolean;
  meta?: Record<string, unknown>;
}

Dependencies

PackagePurpose
@carrot/design-components-calendarCSS styles
@carrot/temporalRecurringEvent type
@carrot/coreShared utilities
vue ^3.5.18Peer dependency

Build

bash
npm run build

Usage Guide

Included in the umbrella. If you use @carrot/design-vue and @carrot/design-components, you already have calendar — no extra install or import needed.

Setup

ts
// Recommended — from the umbrella (nothing extra to install)
import {
  CarrotCalendar,
  CarrotCalendarEvent,
  CarrotCalendarEventPopover,
  type CalendarEventData,
} from "@carrot/design-vue";

// Or from the individual package for explicit tree-shaking
import { CarrotCalendar, type CalendarEventData } from "@carrot/design-vue-calendar";

CSS is included in the @carrot/design-components umbrella. If you use individual CSS packages instead:

ts
import "@carrot/design-components-calendar";

CarrotCalendar — Practical Examples

Basic Month Calendar

vue
<script setup lang="ts">
import { ref } from "vue";
import { CarrotCalendar, type CalendarEventData } from "@carrot/design-vue";

const currentDate = ref("2026-03-22");

const events = ref<CalendarEventData[]>([
  { id: "1", title: "Team standup", start: "2026-03-22T09:00Z", end: "2026-03-22T09:30Z", variant: "accent" },
  { id: "2", title: "Sprint review", start: "2026-03-22", end: "2026-03-22", allDay: true, variant: "success" },
  { id: "3", title: "Holiday", start: "2026-03-25", end: "2026-03-27", allDay: true, color: "#8b5cf6" },
]);
</script>

<template>
  <CarrotCalendar
    v-model="currentDate"
    view="month"
    :events="events"
    week-start="monday"
  />
</template>

Work Week View (5 days, dimmed non-working hours)

vue
<template>
  <CarrotCalendar
    v-model="currentDate"
    view="week"
    :events="events"
    :week-days="5"
    slot-interval="30"
    editable
    :working-hours-start="8"
    :working-hours-end="18"
    working-hours-mode="dim"
  />
</template>

Hidden non-working hours

Set working-hours-mode="hide" to remove non-working hours entirely. The grid only renders the working range (e.g. 08:00–18:00):

vue
<template>
  <CarrotCalendar
    v-model="currentDate"
    view="week"
    :events="events"
    :working-hours-start="8"
    :working-hours-end="22"
    working-hours-mode="hide"
  />
</template>

Day View with Click-to-Create

vue
<script setup lang="ts">
import { ref } from "vue";
import { CarrotCalendar, type CalendarEventData } from "@carrot/design-vue";

const currentDate = ref("2026-03-22");
const events = ref<CalendarEventData[]>([]);

function onSlotClick(date: string, time: string) {
  events.value.push({
    id: `new-${Date.now()}`,
    title: "New event",
    start: `${date}T${time}Z`,
    end: `${date}T${time.replace(/:\d{2}$/, ":30")}Z`,
    variant: "accent",
    editable: true,
  });
}
</script>

<template>
  <CarrotCalendar
    v-model="currentDate"
    view="day"
    :events="events"
    slot-interval="30"
    editable
    @slot:click="onSlotClick"
  />
</template>

Year Overview with Drill-Down

vue
<script setup lang="ts">
import { ref } from "vue";
import { CarrotCalendar, type CalendarView } from "@carrot/design-vue";

const currentDate = ref("2026-03-22");
const view = ref<CalendarView>("year");

function onDateClick(date: string) {
  currentDate.value = date;
  view.value = "day";
}
</script>

<template>
  <CarrotCalendar
    v-model="currentDate"
    v-model:view="view"
    :events="events"
    @date:click="onDateClick"
  />
</template>

Custom Event Rendering

vue
<template>
  <CarrotCalendar v-model="currentDate" :events="events">
    <!-- Custom header -->
    <template #header="{ title, nav }">
      <div style="display: flex; align-items: center; padding: 1rem;">
        <button @click="nav.prev">&lt;</button>
        <h2 style="margin: 0 1rem;">{{ title }}</h2>
        <button @click="nav.next">&gt;</button>
      </div>
    </template>
  </CarrotCalendar>
</template>

Event Popover with Recurring Event Support

vue
<script setup lang="ts">
import { ref } from "vue";
import {
  CarrotCalendar,
  CarrotCalendarEventPopover,
  type CalendarEventData,
} from "@carrot/design-vue";
import { addException, splitEvent } from "@carrot/temporal";

const popoverEvent = ref<CalendarEventData | null>(null);
const popoverAnchor = ref<HTMLElement | null>(null);
const popoverOpen = ref(false);

function onEventClick(event: CalendarEventData, domEvent: MouseEvent) {
  popoverEvent.value = event;
  popoverAnchor.value = domEvent.target as HTMLElement;
  popoverOpen.value = true;
}

function onEditThis() {
  // Edit single instance — add override via @carrot/temporal
  console.log("Edit this instance:", popoverEvent.value?.id);
  popoverOpen.value = false;
}

function onEditFuture() {
  // Split series at this date — use splitEvent() from @carrot/temporal
  console.log("Edit this and future:", popoverEvent.value?.id);
  popoverOpen.value = false;
}
</script>

<template>
  <CarrotCalendar
    v-model="currentDate"
    :events="events"
    @event:click="onEventClick"
  />

  <CarrotCalendarEventPopover
    v-model:open="popoverOpen"
    :event="popoverEvent"
    :anchor="popoverAnchor"
    :is-recurring="!!popoverEvent?.recurrence"
    @edit:this="onEditThis"
    @edit:future="onEditFuture"
  />
</template>

Drag & Drop Events

vue
<script setup lang="ts">
function onEventDrop(event: CalendarEventData, newStart: string, newEnd: string) {
  // Update the event with the new times
  const idx = events.value.findIndex((e) => e.id === event.id);
  if (idx !== -1) {
    events.value[idx] = { ...events.value[idx], start: newStart, end: newEnd };
  }
}

function onEventResize(event: CalendarEventData, newStart: string, newEnd: string) {
  const idx = events.value.findIndex((e) => e.id === event.id);
  if (idx !== -1) {
    events.value[idx] = { ...events.value[idx], start: newStart, end: newEnd };
  }
}
</script>

<template>
  <CarrotCalendar
    v-model="currentDate"
    view="week"
    :events="events"
    editable
    @event:drop="onEventDrop"
    @event:resize="onEventResize"
  />
</template>

Using Composables Directly

For custom calendar layouts, use the composables without the CarrotCalendar wrapper:

vue
<script setup lang="ts">
import {
  useCalendarNavigation,
  useCalendarGrid,
  useCalendarEvents,
  useNowIndicator,
} from "@carrot/design-vue"; // or "@carrot/design-vue-calendar"

const nav = useCalendarNavigation({ view: "week", weekStart: "monday" });
const grid = useCalendarGrid({ slotInterval: "30" });
const { isToday } = useNowIndicator();

// Build your own layout using nav.visibleDays, grid.timeSlots, etc.
</script>

Common Patterns

"+N more" popover

vue
<script setup lang="ts">
function onMoreClick(date: string, events: CalendarEventData[], domEvent: MouseEvent) {
  // Show a dropdown/modal with all events for this date
  console.log(`${events.length} events on ${date}`);
}
</script>

Colour-coding by category

ts
const categoryColors: Record<string, string> = {
  meeting: "#3b82f6",
  deadline: "#ef4444",
  personal: "#8b5cf6",
  travel: "#f59e0b",
};

const events: CalendarEventData[] = tasks.map((task) => ({
  id: task.id,
  title: task.name,
  start: task.start,
  end: task.end,
  color: categoryColors[task.category],
}));

Carrot