implement
Calendar

Calendar

Pick a date from a month grid.

Appointment date August 2026
SMTWTFS
26
27
28
29
30
31
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
1
2
3
4
5
import { signal } from "@implementjs/core";
import { today, type CalendarDate } from "@implementjs/primitives";
import { Calendar } from "@/lib/components/ui/calendar";

export default function CalendarDemo() {
	const value = signal<CalendarDate | null>(today());

	return Calendar({ value, calendarLabel: "Appointment date" });
}

A calendar shows one or more months as a grid of days and owns which date is selected. Calendar is the root; instead of plain children it takes a render function, which receives the visible months and localized weekdays so you can lay out the grid yourself with ForEach:

import {
	Calendar,
	CalendarCell,
	CalendarDay,
	CalendarGrid,
	CalendarGridBody,
	CalendarGridHead,
	CalendarGridRow,
	CalendarHeadCell,
	CalendarHeader,
	CalendarHeading,
	CalendarNextButton,
	CalendarPrevButton,
} from "@implementjs/primitives";
import { ForEach, Fragment } from "@implementjs/core";

Calendar({ calendarLabel: "Appointment date" }, ({ months, weekdays }) =>
	Fragment(
		CalendarHeader(CalendarPrevButton("←"), CalendarHeading(), CalendarNextButton("→")),
		ForEach(
			months,
			(month) => month.value.toString(),
			(month) =>
				CalendarGrid(
					CalendarGridHead(
						CalendarGridRow(
							ForEach(
								weekdays,
								(_, i) => i,
								(weekday) => CalendarHeadCell(weekday),
							),
						),
					),
					CalendarGridBody(
						ForEach(
							month.bind((m) => m.weeks),
							(week) => week[0].toString(),
							(week) =>
								CalendarGridRow(
									ForEach(
										week,
										(date) => date.toString(),
										(date) => CalendarCell({ date, month }, CalendarDay()),
									),
								),
						),
					),
				),
		),
	),
);

The grid renders real table elements — CalendarGrid is a Table with role="grid", CalendarCell a Td, and so on — so the markup stays an accessible calendar table. CalendarDay renders the day number by default; pass children to replace it.

Dates

Dates are plain values of the CalendarDate class the package exports — an immutable year/month/day with no time or time zone attached:

import { CalendarDate, today, parseDate } from "@implementjs/primitives";

const date = new CalendarDate(2026, 8, 20);
date.add({ months: 1 }); // a new CalendarDate; the original is untouched
date.toString(); // "2026-08-20"
parseDate("2026-08-20"); // back to a CalendarDate
today(); // the current local date

add, subtract, set, and compare cover the arithmetic a calendar needs, clamping days that would overflow a month (adding a month to January 31st lands on the last day of February). isSameDay, isSameMonth, isBefore, isAfter, and isBetweenInclusive are exported alongside.

Value

Calendar owns the selected date. Pass a CalendarDate to seed it, or a signal holding CalendarDate | null to control it from outside:

const value = signal<CalendarDate | null>(null);

Calendar({ value }, ({ months, weekdays }) => /* ... */);

value.set(new CalendarDate(2026, 12, 24)); // selects it and moves the view

Clicking a selected date clears it back to null unless preventDeselect is set. With type: "multiple" the value is a Signal<CalendarDate[]> instead, clicks toggle membership, and maxDays caps how many dates can be selected — exceeding it restarts the selection at the clicked date.

Placeholder

The placeholder is the date the view starts on and keyboard focus follows; it is not a selection. Pass a signal to move the view programmatically — selecting a date, navigating, and arrowing across a month boundary all write it back.

CalendarPrevButton and CalendarNextButton page the view one month at a time — or by numberOfMonths when pagedNavigation is set — and disable themselves at minValue/maxValue. CalendarMonthSelect and CalendarYearSelect render native select elements that jump straight to a month or year:

CalendarHeader(CalendarMonthSelect(), CalendarYearSelect());

Keyboard and focus

The focused day is the only Tab stop. Arrow keys move by one day horizontally and one week vertically, paging the calendar when focus crosses the visible months; Enter and Space select. Days disabled by disableDaysOutsideMonth (the default for days outside the month) or by minValue/maxValue/isDateDisabled are skipped.

Disabled and unavailable dates

Three ways to rule dates out:

  • minValue / maxValue bound the selectable window and disable the nav buttons at the edges.
  • isDateDisabled disables matching dates entirely — not selectable, not focusable.
  • isDateUnavailable marks dates that exist but can't be picked (a booked-out day): they stay focusable, get data-unavailable, and announce as disabled.

disabled on the root disables the whole calendar; readonly keeps the value visible but unchangeable.

Internationalization

locale drives every formatted string — the heading, weekday names, and day labels — through Intl.DateTimeFormat. The week starts on the locale's first day where the runtime knows it, or Sunday; override with weekStartsOn. weekdayFormat, monthFormat, and yearFormat pick the Intl widths (or take a function).

Accessibility

The root is a role="application" labeled by calendarLabel plus the visible month, and it renders a visually hidden heading announcing the same. Each day is a role="button" with a full date label ("Saturday, June 15, 2024"). Selections are announced through a polite live region.

Styling

Every part sets a data-calendar-* attribute (data-calendar-root, data-calendar-grid, data-calendar-day, …). Cells and days additionally expose their state:

CalendarDay({
	class:
		"size-8 rounded-md data-selected:bg-primary data-today:bg-accent data-outside-month:text-muted-foreground data-disabled:opacity-50",
});

data-selected, data-today, data-focused, data-outside-month, data-outside-visible-months, data-disabled, and data-unavailable are present when they apply, and data-value holds the cell's ISO date.

API Reference

Calendar

The root. Owns the selected value and the visible months, and calls its children render function with { months, weekdays }. Sets role="application" and a full aria-label. Renders a Div; extra props are forwarded onto it.

PropTypeDefaultDescription
type"single" | "multiple""single"Whether one date is selected, or several can be.
valueSignal<CalendarDate | null> | Signal<CalendarDate[]>The selection. CalendarDate | null when type is "single", CalendarDate[] when "multiple". Pass a signal to control it from outside.
maxDaysnumberOnly for type "multiple": the most dates that can be selected. Exceeding it restarts the selection at the clicked date.
onDateSelect() => voidRuns after a date is selected (not after a deselection).
placeholderSignal<CalendarDate> | CalendarDatetodayThe date the view starts on and keyboard focus follows. Pass a signal to control the view from outside.
minValueCalendarDateThe earliest selectable date. Earlier dates are disabled.
maxValueCalendarDateThe latest selectable date. Later dates are disabled.
isDateDisabled(date: CalendarDate) => booleanMarks dates as disabled: not selectable and skipped by the keyboard.
isDateUnavailable(date: CalendarDate) => booleanMarks dates as unavailable: focusable and rendered, but not selectable. Sets data-unavailable.
preventDeselectbooleanfalseWhether clicking a selected date again keeps it selected.
disabledSignal<boolean> | booleanfalseDisables the whole calendar.
readonlySignal<boolean> | booleanfalseThe value can be read but not changed.
fixedWeeksbooleanfalseAlways render six weeks per month so the grid height never changes.
numberOfMonthsnumber1How many consecutive months are rendered.
pagedNavigationbooleanfalseWhether prev/next move by numberOfMonths months instead of one.
weekStartsOn0 | 1 | 2 | 3 | 4 | 5 | 6The day the week starts on, 0 being Sunday. Defaults to the locale's week start where the runtime knows it.
weekdayFormat"narrow" | "short" | "long""narrow"The Intl width of the weekday names handed to the render function.
disableDaysOutsideMonthbooleantrueWhether the leading and trailing days outside the month are disabled.
localestring"en-US"BCP 47 locale tag used for all formatting.
calendarLabelstring"Event"Prefixed onto the visible month to label the calendar for assistive technology.
monthFormatIntl.DateTimeFormatOptions["month"] | ((month: number) => string)"long"How the heading and month select format month names.
yearFormatIntl.DateTimeFormatOptions["year"] | ((year: number) => string)"numeric"How the heading and year select format years.
Data attributeValue
[data-calendar-root]Present
[data-invalid]Present when the value is disabled or unavailable
[data-disabled]Present when disabled
[data-readonly]Present when readonly

CalendarCell

One grid cell. Sets role="gridcell" plus aria-selected/aria-disabled, and computes the day's state for everything inside it. Renders a Td; extra props are forwarded onto it.

PropTypeDefaultDescription
date*CalendarDate | Readable<CalendarDate>The date this cell renders.
month*CalendarDate | Month | Readable<CalendarDate | Month>The month whose grid the cell sits in — pass the render function's month straight through.
Data attributeValue
[data-calendar-cell]Present
[data-value]The cell's date as YYYY-MM-DD
[data-selected]Present when selected
[data-focused]Present when the placeholder is this date
[data-today]Present on today's date
[data-outside-month]Present on leading/trailing days
[data-outside-visible-months]Present when the date falls outside every rendered month
[data-disabled]Present when disabled
[data-unavailable]Present when unavailable

CalendarDay

The selectable day inside a cell. Sets role="button" with a full date label; renders the day number unless children are passed. The focused day is the calendar's one Tab stop. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-calendar-day]Present
[data-value]The cell's date as YYYY-MM-DD
[data-selected]Present when selected
[data-focused]Present when the placeholder is this date
[data-today]Present on today's date
[data-outside-month]Present on leading/trailing days
[data-outside-visible-months]Present when the date falls outside every rendered month
[data-disabled]Present when disabled
[data-unavailable]Present when unavailable

CalendarHeader

Wraps the heading and the nav buttons. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-calendar-header]Present
[data-disabled]Present when the calendar is disabled
[data-readonly]Present when the calendar is readonly

CalendarHeading

Shows the visible month(s). Renders the formatted heading unless children are passed. aria-hidden — assistive technology hears the root's label instead. Renders a Div; extra props are forwarded onto it.

Data attributeValue
[data-calendar-heading]Present
[data-disabled]Present when the calendar is disabled
[data-readonly]Present when the calendar is readonly

CalendarPrevButton

Pages the view backwards. Disables itself when the previous page falls entirely before minValue. Renders a Button; extra props are forwarded onto it.

Data attributeValue
[data-calendar-prev-button]Present
[data-disabled]Present when disabled

CalendarNextButton

Pages the view forwards. Disables itself when the next page falls entirely after maxValue. Renders a Button; extra props are forwarded onto it.

Data attributeValue
[data-calendar-next-button]Present
[data-disabled]Present when disabled

CalendarGrid

One month's grid. Sets role="grid" and the aria disabled/readonly state. Renders a Table; extra props are forwarded onto it.

Data attributeValue
[data-calendar-grid]Present
[data-disabled]Present when the calendar is disabled
[data-readonly]Present when the calendar is readonly

CalendarGridHead

Holds the weekday header row. Renders a Thead; extra props are forwarded onto it.

Data attributeValue
[data-calendar-grid-head]Present
[data-disabled]Present when the calendar is disabled
[data-readonly]Present when the calendar is readonly

CalendarGridRow

One row: the weekday names in the head, a week in the body. Renders a Tr; extra props are forwarded onto it.

Data attributeValue
[data-calendar-grid-row]Present
[data-disabled]Present when the calendar is disabled
[data-readonly]Present when the calendar is readonly

CalendarHeadCell

One weekday name. Render the strings from the weekdays render prop into these. Renders a Th; extra props are forwarded onto it.

Data attributeValue
[data-calendar-head-cell]Present
[data-disabled]Present when the calendar is disabled
[data-readonly]Present when the calendar is readonly

CalendarGridBody

Holds the week rows. Renders a Tbody; extra props are forwarded onto it.

Data attributeValue
[data-calendar-grid-body]Present
[data-disabled]Present when the calendar is disabled
[data-readonly]Present when the calendar is readonly

CalendarMonthSelect

A native select that jumps the view to a month. Renders localized options for every month unless narrowed with the months prop. Renders a Select; extra props are forwarded onto it.

PropTypeDefaultDescription
monthsnumber[][1 … 12]The month numbers to offer.
monthFormatIntl.DateTimeFormatOptions["month"] | ((month: number) => string)How option labels are formatted. Defaults to the root's monthFormat.
disabledSignal<boolean> | booleanfalsePrevents changing the month.
Data attributeValue
[data-calendar-month-select]Present
[data-disabled]Present when disabled

CalendarYearSelect

A native select that jumps the view to a year. Offers roughly the last hundred years through the next ten, bounded by minValue/maxValue. Renders a Select; extra props are forwarded onto it.

PropTypeDefaultDescription
yearsnumber[]The years to offer. Defaults to a window around the current year.
yearFormatIntl.DateTimeFormatOptions["year"] | ((year: number) => string)How option labels are formatted. Defaults to the root's yearFormat.
disabledSignal<boolean> | booleanfalsePrevents changing the year.
Data attributeValue
[data-calendar-year-select]Present
[data-disabled]Present when disabled