Component
Month calendar
<DomMonthCalendar>A large scrolling month calendar that keeps weekday grids aligned and lets each day render custom UI.
Playground
Try every prop live
Month calendar playground
Edit the range and display props to preview a stacked month grid.
June 2026
July 2026
<script setup>
import { reactive } from 'vue';
import { DomMonthCalendar } from '@getdom/studio/vue';
const data = reactive({
"startDate": "2026-06-11",
"initialMonth": null,
"initialYear": null,
"months": 2,
"locale": "en-GB",
"weekStartsOn": 1,
"weekdayFormat": "short",
"showAdjacentDays": true,
"fixedWeeks": false,
"showWeekNumbers": true,
"mutedBefore": "2026-06-11",
"mutedAfter": "",
"min": "",
"max": "",
"clickable": false,
"clickableAdjacentDays": false,
"dragAndDrop": false,
"dropEffect": "move",
"disabledDate": null,
"dayHeaders": true,
"dayClass": "",
"dayStyle": ""
});
</script>
<template>
<DomMonthCalendar
v-bind="data"
/>
</template>Demo
Coparent calendar
Use dayClass and dayStyle to colour full days, then listen for day-click to change future assignments.
June 2026
July 2026
August 2026
<script setup>
import { ref } from 'vue';
import { DomMonthCalendar } from '../../../lib/vue';
const today = '2026-06-11';
const overrides = ref({});
const handovers = {
'2026-06-12': 'Handover at drama class',
'2026-06-19': 'School pickup at 3:20',
'2026-06-26': 'Linda takes kit bag',
'2026-07-03': 'Steve collects from camp',
'2026-07-10': 'Handover after swimming',
};
function parseDate(value) {
const [year, month, day] = value.split('-').map(Number);
return new Date(year, month - 1, day);
}
function daysBetween(a, b) {
const start = parseDate(a);
const end = parseDate(b);
return Math.round((end - start) / 86400000);
}
function baseParent(value) {
return Math.floor(daysBetween('2026-06-01', value) / 7) % 2 === 0 ? 'Steve' : 'Linda';
}
function parentFor(value) {
return overrides.value[value] || baseParent(value);
}
function toggleParent({ cell }) {
if (!cell.currentMonth || cell.value < today) return;
const current = parentFor(cell.value);
overrides.value = {
...overrides.value,
[cell.value]: current === 'Steve' ? 'Linda' : 'Steve',
};
}
function disabledDate(cell) {
return !cell.currentMonth || cell.value < today;
}
function dayStyle(cell) {
if (!cell.currentMonth) return '';
const parent = parentFor(cell.value);
const past = cell.value < today;
if (parent === 'Steve') {
return { backgroundColor: past ? '#fff1f2' : '#fee2e2' };
}
return { backgroundColor: past ? '#f0fdf4' : '#dcfce7' };
}
function dayClass(cell) {
if (!cell.currentMonth) return '';
return parentFor(cell.value) === 'Steve' ? 'text-red-950' : 'text-emerald-950';
}
</script>
<template>
<div class="w-[76rem] max-w-full" data-testid="coparent-example">
<DomMonthCalendar
start-date="2026-06-11"
:months="3"
:min="today"
:muted-before="today"
clickable
:disabled-date="disabledDate"
:day-style="dayStyle"
:day-class="dayClass"
@day-click="toggleParent"
>
<template #month-header>
<span>Steve / Linda care schedule</span>
</template>
<template #default="{ cell }">
<div v-if="cell.currentMonth" class="flex h-[calc(100%-1.75rem)] flex-col justify-between gap-3">
<div class="text-sm font-bold">
{{ parentFor(cell.value) }}
</div>
<div v-if="handovers[cell.value]" class="rounded-lg border border-current/15 bg-white/70 px-2.5 py-2 text-xs font-semibold leading-4 shadow-xs">
<div class="uppercase tracking-wide opacity-70">Handover</div>
<div class="mt-1">{{ handovers[cell.value] }}</div>
</div>
</div>
</template>
</DomMonthCalendar>
</div>
</template>
Demo
Server-loaded navigation
Navigate month by month while a simulated server request reloads records, then drag one record to a new day without changing existing records on that day.
Server-loaded schedule
June 2026
/api/calendar-records?month=2026-06
June 2026
<script setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { DomIconButton, DomMonthCalendar, DomStatusPill, DomToggle } from '../../../lib/vue';
const cursor = ref(new Date(2026, 5, 1));
const recordsByDate = ref({});
const loading = ref(false);
const weekNumbers = ref(true);
const activeRequestId = ref(0);
const loadedAt = ref('');
const requestLog = ref([]);
let mounted = false;
let logId = 0;
const monthFormatter = new Intl.DateTimeFormat('en-GB', {
month: 'long',
year: 'numeric',
});
const timeFormatter = new Intl.DateTimeFormat('en-GB', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
const visibleMonth = computed(() => monthValue(cursor.value));
const calendarStartDate = computed(() => `${visibleMonth.value}-01`);
const monthLabel = computed(() => monthFormatter.format(cursor.value));
const recordCount = computed(() => Object.values(recordsByDate.value).reduce((total, records) => total + records.length, 0));
const statusTone = computed(() => loading.value ? 'info' : 'success');
const statusLabel = computed(() => loading.value ? `Loading ${visibleMonth.value}` : `${recordCount.value} records loaded`);
onMounted(() => {
mounted = true;
loadMonth(visibleMonth.value);
});
onBeforeUnmount(() => {
mounted = false;
activeRequestId.value += 1;
});
watch(visibleMonth, (value) => {
loadMonth(value);
});
function navigateMonth(delta) {
cursor.value = new Date(cursor.value.getFullYear(), cursor.value.getMonth() + delta, 1);
}
async function loadMonth(month) {
const requestId = activeRequestId.value + 1;
activeRequestId.value = requestId;
loading.value = true;
loadedAt.value = '';
recordsByDate.value = {};
addRequestLog(`GET /api/calendar-records?month=${month}`, 'info');
const records = await emulateServerFetch(month);
if (!mounted || requestId !== activeRequestId.value) {
if (mounted) addRequestLog(`Ignored stale response for ${month}`, 'warning');
return;
}
recordsByDate.value = groupRecords(records);
loadedAt.value = timeFormatter.format(new Date());
loading.value = false;
addRequestLog(`Loaded ${records.length} records for ${month}`, 'success');
}
function emulateServerFetch(month) {
const delay = 460 + ((month.charCodeAt(5) + month.charCodeAt(6)) % 5) * 170;
return new Promise((resolve) => {
window.setTimeout(() => resolve(buildServerRecords(month)), delay);
});
}
function buildServerRecords(month) {
const [year, monthNumber] = month.split('-').map(Number);
const daysInMonth = new Date(year, monthNumber, 0).getDate();
const titles = ['Install window', 'Support cover', 'Renewal review', 'QA handoff', 'Partner launch'];
const teams = ['Field ops', 'Success', 'Revenue', 'Platform', 'Marketing'];
const tones = ['primary', 'success', 'warning', 'info', 'neutral'];
const records = [];
for (let day = 1; day <= daysInMonth; day += 1) {
const seed = (year + monthNumber * 19 + day * 7) % 23;
const date = `${year}-${pad(monthNumber)}-${pad(day)}`;
if ([2, 6, 9, 14, 18].includes(seed)) {
records.push(createRecord(date, day, seed, titles, teams, tones));
}
if ([4, 17].includes(seed)) {
records.push(createRecord(date, day + 3, seed + 5, titles, teams, tones));
}
}
return records;
}
function createRecord(date, day, seed, titles, teams, tones) {
const index = Math.abs(seed + day) % titles.length;
return {
id: `${date}-${seed}-${day}`,
date,
title: titles[index],
team: teams[(index + seed) % teams.length],
tone: tones[(index + day) % tones.length],
time: `${pad(8 + (seed % 8))}:00`,
};
}
function groupRecords(records) {
return records.reduce((groups, record) => {
groups[record.date] = [...(groups[record.date] || []), record];
return groups;
}, {});
}
function recordsFor(value) {
return recordsByDate.value[value] || [];
}
function disabledDate() {
return loading.value;
}
function dayClass(cell) {
if (!cell.currentMonth) return '';
if (loading.value) return 'bg-secondary/25';
if (recordsFor(cell.value).length) return 'bg-primary/5';
return '';
}
function dragDataFor(record) {
return {
id: record.id,
title: record.title,
sourceDate: record.date,
};
}
function moveRecord({ data, targetValue }) {
if (loading.value || !data?.id || !targetValue) return;
const sourceDate = data.sourceDate || recordDate(data.id);
if (!sourceDate || sourceDate === targetValue) return;
const record = recordsByDate.value[sourceDate]?.find((item) => item.id === data.id);
if (!record) return;
const next = { ...recordsByDate.value };
next[sourceDate] = (next[sourceDate] || []).filter((item) => item.id !== record.id);
if (!next[sourceDate].length) delete next[sourceDate];
next[targetValue] = [
...(next[targetValue] || []),
{
...record,
date: targetValue,
},
].sort((a, b) => a.time.localeCompare(b.time));
recordsByDate.value = next;
loadedAt.value = timeFormatter.format(new Date());
addRequestLog(`PATCH /api/calendar-records/${record.id} date=${targetValue}`, 'info');
}
function recordDate(id) {
for (const [date, records] of Object.entries(recordsByDate.value)) {
if (records.some((record) => record.id === id)) return date;
}
return '';
}
function toneClass(record) {
return {
primary: 'border-primary/20 bg-primary/10 text-primary',
success: 'border-success/20 bg-success/10 text-success',
warning: 'border-warning/30 bg-warning/15 text-warning-fg',
info: 'border-primary/20 bg-primary/10 text-primary',
neutral: 'border-border bg-secondary/45 text-canvas-fg',
}[record.tone] || 'border-border bg-secondary/45 text-canvas-fg';
}
function logToneClass(tone) {
return {
info: 'bg-primary',
success: 'bg-success',
warning: 'bg-warning',
}[tone] || 'bg-muted-fg';
}
function addRequestLog(message, tone) {
requestLog.value = [
{ id: ++logId, message, tone },
...requestLog.value,
].slice(0, 4);
}
function monthValue(date) {
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}`;
}
function pad(value) {
return String(value).padStart(2, '0');
}
</script>
<template>
<div class="w-[78rem] max-w-full space-y-4" data-testid="server-loaded-navigation-example">
<section class="rounded-2xl border border-border bg-canvas p-4 shadow-sm">
<div class="flex flex-wrap items-start justify-between gap-4">
<div class="min-w-0">
<p class="text-xs font-semibold uppercase tracking-wide text-muted-fg">Server-loaded schedule</p>
<h3 class="mt-1 text-2xl font-bold tracking-tight text-canvas-fg">{{ monthLabel }}</h3>
<p class="mt-1 text-sm text-muted-fg">/api/calendar-records?month={{ visibleMonth }}</p>
</div>
<div class="flex items-center gap-2">
<DomIconButton
label="Previous month"
icon="M15 18l-6-6 6-6"
variant="secondary"
@click="navigateMonth(-1)"
/>
<DomIconButton
label="Next month"
icon="M9 18l6-6-6-6"
variant="secondary"
@click="navigateMonth(1)"
/>
</div>
</div>
<div class="mt-4 grid gap-4 lg:grid-cols-[1fr_auto] lg:items-center">
<div class="flex flex-wrap items-center gap-3">
<DomStatusPill
:tone="statusTone"
:label="statusLabel"
:pulse="loading"
size="sm"
/>
<span class="text-sm text-muted-fg">
<span v-if="loadedAt">Updated {{ loadedAt }}</span>
<span v-else>Waiting for response</span>
</span>
</div>
<DomToggle v-model="weekNumbers" label="Week numbers" />
</div>
<ul class="mt-4 grid gap-2 text-xs text-muted-fg" aria-live="polite">
<li
v-for="entry in requestLog"
:key="entry.id"
class="flex min-w-0 items-center gap-2 rounded-lg bg-secondary/35 px-3 py-2"
>
<span class="size-1.5 shrink-0 rounded-full" :class="logToneClass(entry.tone)" aria-hidden="true"></span>
<span class="truncate">{{ entry.message }}</span>
</li>
</ul>
</section>
<DomMonthCalendar
:start-date="calendarStartDate"
:months="1"
:fixed-weeks="true"
:show-week-numbers="weekNumbers"
:day-class="dayClass"
:disabled-date="disabledDate"
drag-and-drop
@day-drop="moveRecord"
>
<template #month-header>
<span>{{ loading ? 'Fetching records' : `${recordCount} records available` }}</span>
</template>
<template #default="{ cell, drag, dragAttrs }">
<div v-if="cell.currentMonth" class="min-h-[6.25rem]">
<div v-if="loading" class="space-y-2" aria-hidden="true">
<div class="h-3 w-11/12 animate-pulse rounded-full bg-secondary"></div>
<div class="h-3 w-7/12 animate-pulse rounded-full bg-secondary"></div>
<div class="h-8 w-full animate-pulse rounded-lg bg-secondary/70"></div>
</div>
<div v-else-if="recordsFor(cell.value).length" class="space-y-2">
<article
v-for="record in recordsFor(cell.value)"
:key="record.id"
v-bind="dragAttrs(dragDataFor(record))"
class="cursor-grab rounded-lg border px-2 py-1.5 text-xs font-semibold leading-4 transition active:cursor-grabbing"
:class="[
toneClass(record),
drag.source && drag.data?.id === record.id && 'opacity-60 ring-2 ring-ring/40',
]"
>
<div class="flex min-w-0 items-center justify-between gap-2">
<span class="min-w-0 truncate">{{ record.title }}</span>
<span class="shrink-0 opacity-75">{{ record.time }}</span>
</div>
<div class="mt-1 truncate text-[11px] font-medium opacity-75">{{ record.team }}</div>
</article>
</div>
<div v-else class="text-xs text-muted-fg">No records</div>
</div>
</template>
</DomMonthCalendar>
</div>
</template>
Demo
Content planner
Render article planning cards through the default day slot, then use day-drop payloads to move or swap planned content between days.
June 2026
controlled components
tailwind css themes
examples of a sidebar
tailwind css components
component testing
border color in css
switch button css
code splitting
component composition
react loading bar
vue component library
accessibility audit
js progress bar
performance optimization
css variables
component examples
screen reader testing
high contrast mode
accessible dropdown menu
design system tools
July 2026
menu component
accessibility testing tools
color contrast accessibility
command palette
drawer component
<script setup>
import { computed, ref } from 'vue';
import { DomMonthCalendar } from '../../../lib/vue';
const startDate = '2026-06-11';
const initialArticles = [
{ date: '2026-06-11', type: 'Guide: Explainer', title: 'controlled components', volume: '170', difficulty: 14, cta: 'Visit Article' },
{ date: '2026-06-12', type: 'Guide: How-to', title: 'tailwind css themes', volume: '390', difficulty: 24, cta: 'View Article' },
{ date: '2026-06-13', type: 'List: Examples', title: 'examples of a sidebar', volume: '590', difficulty: 22 },
{ date: '2026-06-14', type: 'List: Resources', title: 'tailwind css components', volume: '390', difficulty: 45 },
{ date: '2026-06-15', type: 'Guide: How-to', title: 'component testing', volume: '480', difficulty: 26 },
{ date: '2026-06-16', type: 'Guide: Explainer', title: 'border color in css', volume: '590', difficulty: 16 },
{ date: '2026-06-17', type: 'Guide: How-to', title: 'switch button css', volume: '590', difficulty: 10 },
{ date: '2026-06-18', type: 'Guide: Explainer', title: 'code splitting', volume: '210', difficulty: 30 },
{ date: '2026-06-19', type: 'Guide: Explainer', title: 'component composition', volume: '50', difficulty: 18 },
{ date: '2026-06-20', type: 'Guide: How-to', title: 'react loading bar', volume: '590', difficulty: 17 },
{ date: '2026-06-21', type: 'Guide: Explainer', title: 'vue component library', volume: '320', difficulty: 30 },
{ date: '2026-06-22', type: 'Guide: How-to', title: 'accessibility audit', volume: '150', difficulty: 26 },
{ date: '2026-06-23', type: 'Guide: How-to', title: 'js progress bar', volume: '590', difficulty: 29 },
{ date: '2026-06-24', type: 'Guide: Explainer', title: 'performance optimization', volume: '320', difficulty: 33 },
{ date: '2026-06-25', type: 'Guide: Explainer', title: 'css variables', volume: '2,900', difficulty: 0 },
{ date: '2026-06-26', type: 'List: Examples', title: 'component examples', volume: '390', difficulty: 39 },
{ date: '2026-06-27', type: 'Guide: How-to', title: 'screen reader testing', volume: '260', difficulty: 18 },
{ date: '2026-06-28', type: 'Guide: Explainer', title: 'high contrast mode', volume: '920', difficulty: 24 },
{ date: '2026-06-29', type: 'Guide: How-to', title: 'accessible dropdown menu', volume: '70', difficulty: 15 },
{ date: '2026-06-30', type: 'List: Resources', title: 'design system tools', volume: '210', difficulty: 24 },
{ date: '2026-07-01', type: 'Guide: Explainer', title: 'menu component', volume: '70', difficulty: 38 },
{ date: '2026-07-02', type: 'List: Resources', title: 'accessibility testing tools', volume: '50', difficulty: 40 },
{ date: '2026-07-03', type: 'Guide: Explainer', title: 'color contrast accessibility', volume: '390', difficulty: 52 },
{ date: '2026-07-04', type: 'Guide: Explainer', title: 'command palette', volume: '760', difficulty: 24 },
{ date: '2026-07-05', type: 'Guide: Explainer', title: 'drawer component', volume: '140', difficulty: 30 },
];
const articles = ref(initialArticles.map((article, index) => ({
id: `article-${index + 1}`,
...article,
})));
const articleByDate = computed(() => Object.fromEntries(articles.value.map((article) => [article.date, article])));
function articleFor(value) {
return articleByDate.value[value];
}
function dragDataFor(article) {
return {
id: article.id,
title: article.title,
sourceDate: article.date,
};
}
function moveArticle({ data, sourceValue, targetValue }) {
if (!data?.id || !targetValue || sourceValue === targetValue) return;
const sourceArticle = articles.value.find((article) => article.id === data.id);
if (!sourceArticle) return;
const sourceDate = sourceValue || sourceArticle.date;
const targetArticle = articleFor(targetValue);
articles.value = articles.value.map((article) => {
if (article.id === sourceArticle.id) {
return {
...article,
date: targetValue,
};
}
if (targetArticle && article.id === targetArticle.id) {
return {
...article,
date: sourceDate,
};
}
return article;
});
}
function badgeClass(article) {
return article.type.startsWith('List:')
? 'border-amber-300 bg-amber-50 text-amber-800'
: 'border-sky-300 bg-sky-50 text-sky-800';
}
</script>
<template>
<div class="w-[78rem] max-w-full" data-testid="content-planner-example">
<DomMonthCalendar
:start-date="startDate"
:months="2"
:muted-before="startDate"
drag-and-drop
@day-drop="moveArticle"
>
<template #month-header>
<span>Articles are generated and published at 7AM-9AM UTC.</span>
</template>
<template #default="{ cell, drag, dragAttrs }">
<article
v-for="article in cell.currentMonth && articleFor(cell.value) ? [articleFor(cell.value)] : []"
:key="article.id"
v-bind="dragAttrs(dragDataFor(article))"
class="flex min-h-[7.4rem] cursor-grab flex-col rounded-lg border border-violet-200 bg-violet-50/35 p-3 text-sm shadow-xs transition active:cursor-grabbing"
:class="drag.source && drag.data?.id === article.id && 'opacity-60 ring-2 ring-ring/40'"
>
<div
class="mb-2 w-fit rounded-full border px-1.5 py-0.5 text-[11px] font-bold leading-none"
:class="badgeClass(article)"
>
{{ article.type }}
</div>
<h3 class="text-sm font-bold leading-5 text-canvas-fg">
{{ article.title }}
</h3>
<div class="mt-auto flex items-center justify-between border-t border-violet-100 pt-2 text-xs text-muted-fg">
<span>Vol: <strong class="text-canvas-fg">{{ article.volume }}</strong></span>
<span>Diff: <strong class="text-canvas-fg">{{ article.difficulty }}</strong></span>
</div>
<a
v-if="article.cta"
href="#"
class="mt-2 inline-flex h-8 items-center justify-center rounded-md bg-primary px-3 text-xs font-bold text-primary-fg transition hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
@click.prevent
>
{{ article.cta }}
</a>
</article>
</template>
</DomMonthCalendar>
</div>
</template>
Architecture
Server calendar API
The month calendar is transport-free: it renders date cells and exposes slots. The application owns fetching, loading, empty, and mutation states, then looks up records for each cell.value. For backend work, make the endpoint range-based so one contract supports single-month, multi-month, fixed-week, and adjacent-day displays.
Use date-only strings for all-day placement, and include local times only when the record needs them. That keeps API authors out of timezone edge cases while still allowing the UI to render appointments, availability, content plans, rota assignments, or booking inventory.
For drag and drop, keep the mutation policy in the app layer. The day slot exposes dragAttrs(data) for draggable records, and day-drop emits the source day, target day, transfer data, and native event so a planner can swap records while a server-backed schedule can update only the dragged record.
Read endpoint
GET /api/calendar-records?start=2026-06-01&end=2026-06-30&timezone=Europe/London&resource=field-ops
// Response
{
range: {
start: '2026-06-01',
end: '2026-06-30',
timezone: 'Europe/London',
resource: 'field-ops',
},
data: [
{
id: 'evt_1042',
date: '2026-06-11',
title: 'Install window',
startTime: '09:00',
endTime: '11:00',
status: 'confirmed',
tone: 'primary',
resourceId: 'field-ops',
meta: {
customerId: 'cus_8821',
location: 'Dock 4',
},
},
],
byDate: {
'2026-06-11': ['evt_1042'],
},
meta: {
requestedAt: '2026-06-18T09:30:00.000Z',
etag: 'calendar-records:field-ops:2026-06',
total: 1,
},
}Mutation endpoint
POST /api/calendar-records
// Create or update an item
{
id: 'evt_1042',
date: '2026-06-12',
title: 'Install window',
startTime: '10:00',
endTime: '12:00',
status: 'confirmed',
resourceId: 'field-ops',
clientMutationId: 'calendar-edit-8f83',
}
// Response
{
data: {
id: 'evt_1042',
date: '2026-06-12',
title: 'Install window',
startTime: '10:00',
endTime: '12:00',
status: 'confirmed',
resourceId: 'field-ops',
updatedAt: '2026-06-18T09:31:00.000Z',
},
meta: {
clientMutationId: 'calendar-edit-8f83',
invalidatedRange: {
start: '2026-06-01',
end: '2026-06-30',
},
},
}Now route sketch
// server/api/calendar-records.get.js
export async function get(req, res) {
const range = normalizeCalendarRange(req.query);
if (!range) {
res.status(400);
return { error: 'Use YYYY-MM-DD start and end query parameters.' };
}
const records = await listCalendarRecords({
start: range.start,
end: range.end,
timezone: range.timezone,
resource: req.query.resource || '',
});
return {
range,
data: records,
byDate: groupRecordIdsByDate(records),
meta: {
requestedAt: new Date().toISOString(),
total: records.length,
},
};
}Reference
Props
Control props
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
locale | string | string | 'en-GB' | Locale used for month, weekday, and date labels. |
weekStartsOn | 0 | 1 | 2 | 3 | 4 | 5 | 6 | number | 1 | First day of week. 0 is Sunday, 1 is Monday. |
weekdayFormat | 'narrow' | 'short' | 'long' | string | 'short' | Weekday label length. |
showAdjacentDays | boolean | boolean | true | Show days from the previous and next months so the grid keeps its weekday alignment. |
fixedWeeks | boolean | boolean | false | Render six weeks per month even when the month needs fewer rows. |
showWeekNumbers | boolean | boolean | false | Render ISO-8601 week numbers at the start of each week row. |
mutedBefore | string | string | '' | Dates before this YYYY-MM-DD value render muted. |
mutedAfter | string | string | '' | Dates after this YYYY-MM-DD value render muted. |
dayHeaders | boolean | boolean | true | Render weekday headers. |
dayClass | string | array | object | function | string | Array<unknown> | Record<string, boolean> | ((cell: | '' | Extra classes, or a function that receives a day cell and returns classes for the day cell. |
dayStyle | string | object | function | string | Record<string, string | number> | ((cell: | '' | Extra styles, or a function that receives a day cell and returns styles for the day cell. |
Interaction
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
min | string | string | '' | Minimum clickable date as YYYY-MM-DD. |
max | string | string | '' | Maximum clickable date as YYYY-MM-DD. |
clickable | boolean | boolean | false | Render days as keyboard-reachable buttons and emit day-click. |
clickableAdjacentDays | boolean | boolean | false | Allow day-click on visible days outside the displayed month. |
dragAndDrop | boolean | boolean | false | Expose dragAttrs in the day slot and emit day-drop when a draggable item is dropped onto a day. |
dropEffect | 'copy' | 'move' | 'link' | string | 'move' | Native drop effect shown when a day can receive a dragged item. |
disabledDate | function | (cell: | — | Function that receives a day cell and returns true when the day should not be clickable or droppable. |
Range
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
startDate | string | string | '' | First date in the rendered range as YYYY-MM-DD. The first month starts on the week containing this date. |
initialMonth | number | number | — | Initial month, 1-12. Used when startDate is not set. |
initialYear | number | number | — | Initial year. Used when startDate is not set. |
months | number | number | 3 | Number of consecutive months to render. |
Auto-generated from Month calendar.props and inline _edit hints.
Events
| Name | Payload | Description |
|---|---|---|
| @day-click | ({ | Fired when a clickable day is activated. |
| @day-drag-start | ({ | Fired when an item using dragAttrs starts dragging from a day. |
| @day-drag-enter | ({ | Fired when a dragged item previews a day as the drop target. |
| @day-drag-leave | ({ | Fired when a dragged item leaves a previewed day. |
| @day-drop | ({ | Fired when a draggable item is dropped onto a day. The calendar does not mutate records. |
| @day-drag-end | ({ | Fired when a drag started with dragAttrs ends. |
Names auto-detected from defineEmits and source emit() calls; payload and description from __doc.events when present.
Slots
| Name | Scope | Description |
|---|---|---|
| #(default) | { | Custom content inside each visible calendar day. Use dragAttrs(data) on items that should be draggable. |
| #month-header | { month } | Custom content rendered opposite the month title. |
Keyboard
- TabMove through clickable days when clickable is enabled.
- Enter / SpaceActivate the focused day.