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

8Mon
9Tue
10Wed
11Thu
12Fri
13Sat
14Sun
15Mon
16Tue
17Wed
18Thu
19Fri
20Sat
21Sun
22Mon
23Tue
24Wed
25Thu
26Fri
27Sat
28Sun
29Mon
30Tue
1Wed
2Thu
3Fri
4Sat
5Sun

July 2026

29Mon
30Tue
1Wed
2Thu
3Fri
4Sat
5Sun
6Mon
7Tue
8Wed
9Thu
10Fri
11Sat
12Sun
13Mon
14Tue
15Wed
16Thu
17Fri
18Sat
19Sun
20Mon
21Tue
22Wed
23Thu
24Fri
25Sat
26Sun
27Mon
28Tue
29Wed
30Thu
31Fri
1Sat
2Sun
Playground.vuevue
<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,
	  "mutedBefore": "2026-06-11",
	  "mutedAfter": "",
	  "min": "",
	  "max": "",
	  "clickable": false,
	  "clickableAdjacentDays": false,
	  "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

Steve / Linda care schedule

July 2026

Steve / Linda care schedule

August 2026

Steve / Linda care schedule
<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

Content planner

Render article planning cards through the default day slot while the calendar handles month layout and muted dates.

June 2026

Articles are generated and published at 7AM-9AM UTC.
8Mon
9Tue
10Wed
11Thu
Guide: Explainer

controlled components

Vol: 170Diff: 14
Visit Article
12Fri
Guide: How-to

tailwind css themes

Vol: 390Diff: 24
View Article
13Sat
List: Examples

examples of a sidebar

Vol: 590Diff: 22
14Sun
List: Resources

tailwind css components

Vol: 390Diff: 45
15Mon
Guide: How-to

component testing

Vol: 480Diff: 26
16Tue
Guide: Explainer

border color in css

Vol: 590Diff: 16
17Wed
Guide: How-to

switch button css

Vol: 590Diff: 10
18Thu
Guide: Explainer

code splitting

Vol: 210Diff: 30
19Fri
Guide: Explainer

component composition

Vol: 50Diff: 18
20Sat
Guide: How-to

react loading bar

Vol: 590Diff: 17
21Sun
Guide: Explainer

vue component library

Vol: 320Diff: 30
22Mon
Guide: How-to

accessibility audit

Vol: 150Diff: 26
23Tue
Guide: How-to

js progress bar

Vol: 590Diff: 29
24Wed
Guide: Explainer

performance optimization

Vol: 320Diff: 33
25Thu
Guide: Explainer

css variables

Vol: 2,900Diff: 0
26Fri
List: Examples

component examples

Vol: 390Diff: 39
27Sat
Guide: How-to

screen reader testing

Vol: 260Diff: 18
28Sun
Guide: Explainer

high contrast mode

Vol: 920Diff: 24
29Mon
Guide: How-to

accessible dropdown menu

Vol: 70Diff: 15
30Tue
List: Resources

design system tools

Vol: 210Diff: 24
1Wed
2Thu
3Fri
4Sat
5Sun

July 2026

Articles are generated and published at 7AM-9AM UTC.
29Mon
30Tue
1Wed
Guide: Explainer

menu component

Vol: 70Diff: 38
2Thu
List: Resources

accessibility testing tools

Vol: 50Diff: 40
3Fri
Guide: Explainer

color contrast accessibility

Vol: 390Diff: 52
4Sat
Guide: Explainer

command palette

Vol: 760Diff: 24
5Sun
Guide: Explainer

drawer component

Vol: 140Diff: 30
6Mon
7Tue
8Wed
9Thu
10Fri
11Sat
12Sun
13Mon
14Tue
15Wed
16Thu
17Fri
18Sat
19Sun
20Mon
21Tue
22Wed
23Thu
24Fri
25Sat
26Sun
27Mon
28Tue
29Wed
30Thu
31Fri
1Sat
2Sun
<script setup>
import { computed } from 'vue';
import { DomMonthCalendar } from '../../../lib/vue';

const startDate = '2026-06-11';
const articles = [
	{ 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 articleByDate = computed(() => Object.fromEntries(articles.map((article) => [article.date, article])));

function articleFor(value) {
	return articleByDate.value[value];
}

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"
		>
			<template #month-header>
				<span>Articles are generated and published at 7AM-9AM UTC.</span>
			</template>

			<template #default="{ cell }">
				<article
					v-if="cell.currentMonth && articleFor(cell.value)"
					class="flex min-h-[7.4rem] flex-col rounded-lg border border-violet-200 bg-violet-50/35 p-3 text-sm shadow-xs"
				>
					<div
						class="mb-2 w-fit rounded-full border px-1.5 py-0.5 text-[11px] font-bold leading-none"
						:class="badgeClass(articleFor(cell.value))"
					>
						{{ articleFor(cell.value).type }}
					</div>

					<h3 class="text-sm font-bold leading-5 text-fg">
						{{ articleFor(cell.value).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-fg">{{ articleFor(cell.value).volume }}</strong></span>
						<span>Diff: <strong class="text-fg">{{ articleFor(cell.value).difficulty }}</strong></span>
					</div>

					<a
						v-if="articleFor(cell.value).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
					>
						{{ articleFor(cell.value).cta }}
					</a>
				</article>
			</template>
		</DomMonthCalendar>
	</div>
</template>

Reference

Props

Control props

NameTypeTSDefaultDescription
localestringstring'en-GB'Locale used for month, weekday, and date labels.
weekStartsOn0 | 1 | 2 | 3 | 4 | 5 | 6number1First day of week. 0 is Sunday, 1 is Monday.
weekdayFormat'narrow' | 'short' | 'long'string'short'Weekday label length.
showAdjacentDaysbooleanbooleantrueShow days from the previous and next months so the grid keeps its weekday alignment.
fixedWeeksbooleanbooleanfalseRender six weeks per month even when the month needs fewer rows.
mutedBeforestringstring''Dates before this YYYY-MM-DD value render muted.
mutedAfterstringstring''Dates after this YYYY-MM-DD value render muted.
dayHeadersbooleanbooleantrueRender weekday headers.
dayClassstring | array | object | functionstring | Array<unknown> | Record<string, boolean> | ((cell: MonthCalendarCell
type MonthCalendarCell = {
	date: Date; // Local Date instance for the cell.
	value: string; // YYYY-MM-DD date key.
	day: number; // Day of the month.
	weekday: string; // Formatted weekday label.
	label: string; // Full accessible date label.
	currentMonth: boolean; // True when the date belongs to the rendered month.
	hidden: boolean; // True for adjacent dates when showAdjacentDays is false.
	monthValue: string; // YYYY-MM month key for the rendered month.
	row: number; // Zero-based week row in the grid.
	column: number; // Zero-based weekday column in the grid.
	today: boolean;
	past: boolean;
	future: boolean;
	muted: boolean;
	disabled: boolean;
};
) => unknown)
''Extra classes, or a function that receives a day cell and returns classes for the day cell.
dayStylestring | object | functionstring | Record<string, string | number> | ((cell: MonthCalendarCell
type MonthCalendarCell = {
	date: Date; // Local Date instance for the cell.
	value: string; // YYYY-MM-DD date key.
	day: number; // Day of the month.
	weekday: string; // Formatted weekday label.
	label: string; // Full accessible date label.
	currentMonth: boolean; // True when the date belongs to the rendered month.
	hidden: boolean; // True for adjacent dates when showAdjacentDays is false.
	monthValue: string; // YYYY-MM month key for the rendered month.
	row: number; // Zero-based week row in the grid.
	column: number; // Zero-based weekday column in the grid.
	today: boolean;
	past: boolean;
	future: boolean;
	muted: boolean;
	disabled: boolean;
};
) => string | Record<string, string | number> | null | undefined)
''Extra styles, or a function that receives a day cell and returns styles for the day cell.

Interaction

NameTypeTSDefaultDescription
minstringstring''Minimum clickable date as YYYY-MM-DD.
maxstringstring''Maximum clickable date as YYYY-MM-DD.
clickablebooleanbooleanfalseRender days as keyboard-reachable buttons and emit day-click.
clickableAdjacentDaysbooleanbooleanfalseAllow day-click on visible days outside the displayed month.
disabledDatefunction(cell: MonthCalendarCell
type MonthCalendarCell = {
	date: Date; // Local Date instance for the cell.
	value: string; // YYYY-MM-DD date key.
	day: number; // Day of the month.
	weekday: string; // Formatted weekday label.
	label: string; // Full accessible date label.
	currentMonth: boolean; // True when the date belongs to the rendered month.
	hidden: boolean; // True for adjacent dates when showAdjacentDays is false.
	monthValue: string; // YYYY-MM month key for the rendered month.
	row: number; // Zero-based week row in the grid.
	column: number; // Zero-based weekday column in the grid.
	today: boolean;
	past: boolean;
	future: boolean;
	muted: boolean;
	disabled: boolean;
};
) => boolean
Function that receives a day cell and returns true when the day should not be clickable.

Range

NameTypeTSDefaultDescription
startDatestringstring''First date in the rendered range as YYYY-MM-DD. The first month starts on the week containing this date.
initialMonthnumbernumberInitial month, 1-12. Used when startDate is not set.
initialYearnumbernumberInitial year. Used when startDate is not set.
monthsnumbernumber3Number of consecutive months to render.

Auto-generated from Month calendar.props and inline _edit hints.

Events

NamePayloadDescription
@day-click({ cell
type MonthCalendarCell = {
	date: Date; // Local Date instance for the cell.
	value: string; // YYYY-MM-DD date key.
	day: number; // Day of the month.
	weekday: string; // Formatted weekday label.
	label: string; // Full accessible date label.
	currentMonth: boolean; // True when the date belongs to the rendered month.
	hidden: boolean; // True for adjacent dates when showAdjacentDays is false.
	monthValue: string; // YYYY-MM month key for the rendered month.
	row: number; // Zero-based week row in the grid.
	column: number; // Zero-based weekday column in the grid.
	today: boolean;
	past: boolean;
	future: boolean;
	muted: boolean;
	disabled: boolean;
};
, event
event: MouseEvent;
})
Fired when a clickable day is activated.

Names auto-detected from defineEmits and source emit() calls; payload and description from __doc.events when present.

Slots

NameScopeDescription
#(default){ cell
type MonthCalendarCell = {
	date: Date; // Local Date instance for the cell.
	value: string; // YYYY-MM-DD date key.
	day: number; // Day of the month.
	weekday: string; // Formatted weekday label.
	label: string; // Full accessible date label.
	currentMonth: boolean; // True when the date belongs to the rendered month.
	hidden: boolean; // True for adjacent dates when showAdjacentDays is false.
	monthValue: string; // YYYY-MM month key for the rendered month.
	row: number; // Zero-based week row in the grid.
	column: number; // Zero-based weekday column in the grid.
	today: boolean;
	past: boolean;
	future: boolean;
	muted: boolean;
	disabled: boolean;
};
, month
type MonthCalendarMonth = {
	date: Date; // Local Date instance for the first day of the rendered month.
	value: string; // YYYY-MM month key.
	id: string; // Element id used to label the month grid.
	month: number; // Month number, 1-12.
	year: number; // Four-digit year.
	label: string; // Formatted month heading.
	cells: Array<MonthCalendarCell
>; // Flat list of visible day cells. weeks: Array<MonthCalendarWeek
>; // Day cells grouped into week rows. };
}
Custom content inside each visible calendar day.
#month-header{ month }Custom content rendered opposite the month title.

Keyboard

  • TabMove through clickable days when clickable is enabled.
  • Enter / SpaceActivate the focused day.