Calendar
Pick a date from a month grid.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
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.
Navigation
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/maxValuebound the selectable window and disable the nav buttons at the edges.isDateDisableddisables matching dates entirely — not selectable, not focusable.isDateUnavailablemarks dates that exist but can't be picked (a booked-out day): they stay focusable, getdata-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.
| Prop | Type | Default | Description |
|---|---|---|---|
type | "single" | "multiple" | "single" | Whether one date is selected, or several can be. |
value | Signal<CalendarDate | null> | Signal<CalendarDate[]> | — | The selection. CalendarDate | null when type is "single", CalendarDate[] when "multiple". Pass a signal to control it from outside. |
maxDays | number | — | Only for type "multiple": the most dates that can be selected. Exceeding it restarts the selection at the clicked date. |
onDateSelect | () => void | — | Runs after a date is selected (not after a deselection). |
placeholder | Signal<CalendarDate> | CalendarDate | today | The date the view starts on and keyboard focus follows. Pass a signal to control the view from outside. |
minValue | CalendarDate | — | The earliest selectable date. Earlier dates are disabled. |
maxValue | CalendarDate | — | The latest selectable date. Later dates are disabled. |
isDateDisabled | (date: CalendarDate) => boolean | — | Marks dates as disabled: not selectable and skipped by the keyboard. |
isDateUnavailable | (date: CalendarDate) => boolean | — | Marks dates as unavailable: focusable and rendered, but not selectable. Sets data-unavailable. |
preventDeselect | boolean | false | Whether clicking a selected date again keeps it selected. |
disabled | Signal<boolean> | boolean | false | Disables the whole calendar. |
readonly | Signal<boolean> | boolean | false | The value can be read but not changed. |
fixedWeeks | boolean | false | Always render six weeks per month so the grid height never changes. |
numberOfMonths | number | 1 | How many consecutive months are rendered. |
pagedNavigation | boolean | false | Whether prev/next move by numberOfMonths months instead of one. |
weekStartsOn | 0 | 1 | 2 | 3 | 4 | 5 | 6 | — | The 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. |
disableDaysOutsideMonth | boolean | true | Whether the leading and trailing days outside the month are disabled. |
locale | string | "en-US" | BCP 47 locale tag used for all formatting. |
calendarLabel | string | "Event" | Prefixed onto the visible month to label the calendar for assistive technology. |
monthFormat | Intl.DateTimeFormatOptions["month"] | ((month: number) => string) | "long" | How the heading and month select format month names. |
yearFormat | Intl.DateTimeFormatOptions["year"] | ((year: number) => string) | "numeric" | How the heading and year select format years. |
| Data attribute | Value |
|---|---|
[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.
| Prop | Type | Default | Description |
|---|---|---|---|
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 attribute | Value |
|---|---|
[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 attribute | Value |
|---|---|
[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 attribute | Value |
|---|---|
[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 attribute | Value |
|---|---|
[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 attribute | Value |
|---|---|
[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 attribute | Value |
|---|---|
[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 attribute | Value |
|---|---|
[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 attribute | Value |
|---|---|
[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 attribute | Value |
|---|---|
[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 attribute | Value |
|---|---|
[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 attribute | Value |
|---|---|
[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.
| Prop | Type | Default | Description |
|---|---|---|---|
months | number[] | [1 … 12] | The month numbers to offer. |
monthFormat | Intl.DateTimeFormatOptions["month"] | ((month: number) => string) | — | How option labels are formatted. Defaults to the root's monthFormat. |
disabled | Signal<boolean> | boolean | false | Prevents changing the month. |
| Data attribute | Value |
|---|---|
[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.
| Prop | Type | Default | Description |
|---|---|---|---|
years | number[] | — | The years to offer. Defaults to a window around the current year. |
yearFormat | Intl.DateTimeFormatOptions["year"] | ((year: number) => string) | — | How option labels are formatted. Defaults to the root's yearFormat. |
disabled | Signal<boolean> | boolean | false | Prevents changing the year. |
| Data attribute | Value |
|---|---|
[data-calendar-year-select] | Present |
[data-disabled] | Present when disabled |