Calendar
A calendar panel component built on the native Temporal API, supporting controlled selection, disabled dates and parts-based assembly
Installation
- $npx litefy@latest add calendar
- $pnpm dlx litefy@latest add calendar
- $yarn dlx litefy@latest add calendar
- $bun --bun litefy@latest add calendar
[!TIP] The component uses the browser-native
Temporal.PlainDateAPI. No date library is bundled and no polyfill is provided — make sure the target environment supports the Temporal API (Chrome 139+, or a polyfill installed by yourself).
Usage
Hold visibleMonth in state and update it via onVisibleMonthChange.
Selected: -
"use client";import { useState } from "react";import { Calendar } from "@/ui";export default function Demo() { const [visibleMonth, setVisibleMonth] = useState(() => Temporal.Now.plainDateISO()); const [selectedDate, setSelectedDate] = useState<Temporal.PlainDate | null>(null); return ( <div className="flex flex-col gap-3"> <Calendar visibleMonth={visibleMonth} onVisibleMonthChange={setVisibleMonth} value={selectedDate} onChange={setSelectedDate} firstDayOfWeek={1} /> <p className="text-sm text-muted-foreground"> Selected: {selectedDate ? selectedDate.toString() : "-"} </p> </div> );}Disabled Dates
Return true from isDateDisabled to disable specific dates.
Past dates and weekends are disabled
"use client";import { useState } from "react";import { Calendar } from "@/ui";export default function Demo() { const [visibleMonth, setVisibleMonth] = useState(() => Temporal.Now.plainDateISO()); const [selectedDate, setSelectedDate] = useState<Temporal.PlainDate | null>(null); const isDateDisabled = (date: Temporal.PlainDate) => { const today = Temporal.Now.plainDateISO(); return Temporal.PlainDate.compare(date, today) < 0 || date.dayOfWeek > 5; }; return ( <div className="flex flex-col gap-3"> <Calendar visibleMonth={visibleMonth} onVisibleMonthChange={setVisibleMonth} value={selectedDate} onChange={setSelectedDate} isDateDisabled={isDateDisabled} /> <p className="text-sm text-muted-foreground"> Past dates and weekends are disabled </p> </div> );}Parts Assembly
Compose the exported parts manually to build a fully customized panel.
"use client";
import { useState } from "react";
import {
type CalendarView,
CalendarGrid,
CalendarHeader,
CalendarMonthGrid,
CalendarNavButton,
CalendarRoot,
CalendarTitleButton,
CalendarYearGrid,
calendarMonthLabels,
} from "@/ui";
export default function Demo() {
const [view, setView] = useState<CalendarView>("days");
const [visibleMonth, setVisibleMonth] = useState(() => Temporal.Now.plainDateISO());
const [selectedDate, setSelectedDate] = useState<Temporal.PlainDate | null>(() =>
Temporal.Now.plainDateISO(),
);
const firstOfMonth = visibleMonth.with({ day: 1 });
const offset = firstOfMonth.dayOfWeek % 7;
const start = firstOfMonth.subtract({ days: offset });
const weekCount = Math.ceil((offset + visibleMonth.daysInMonth) / 7);
const weeks = Array.from({ length: weekCount }, (_, weekIndex) =>
Array.from({ length: 7 }, (_, dayIndex) => start.add({ days: weekIndex * 7 + dayIndex })),
);
const handleSelect = (date: Temporal.PlainDate) => {
if (date.year !== visibleMonth.year || date.month !== visibleMonth.month) return;
setSelectedDate(date);
};
const handleMonthSelect = (month: Temporal.PlainDate) => {
setVisibleMonth(month);
setView("days");
};
const handleYearSelect = (year: Temporal.PlainDate) => {
setVisibleMonth(visibleMonth.with({ year: year.year, day: 1 }));
setView("months");
};
const handlePrevious = () => {
if (view === "days") setVisibleMonth(visibleMonth.subtract({ months: 1 }));
else if (view === "months") setVisibleMonth(visibleMonth.subtract({ years: 1 }));
else setVisibleMonth(visibleMonth.subtract({ years: 12 }));
};
const handleNext = () => {
if (view === "days") setVisibleMonth(visibleMonth.add({ months: 1 }));
else if (view === "months") setVisibleMonth(visibleMonth.add({ years: 1 }));
else setVisibleMonth(visibleMonth.add({ years: 12 }));
};
const navUnit = view === "days" ? "month" : view === "months" ? "year" : "years";
return (
<CalendarRoot className="rounded-md border-2 border-primary p-4">
<CalendarHeader>
<CalendarNavButton
direction="previous"
label={`Previous ${navUnit}`}
onClick={handlePrevious}
/>
<div className="flex flex-1 items-center justify-center gap-1">
<CalendarTitleButton
data-active={view === "years" || undefined}
onClick={() => setView("years")}
>
{visibleMonth.year}
</CalendarTitleButton>
<CalendarTitleButton
data-active={view === "months" || undefined}
onClick={() => setView("months")}
>
{calendarMonthLabels[visibleMonth.month - 1]}
</CalendarTitleButton>
</div>
<CalendarNavButton direction="next" label={`Next ${navUnit}`} onClick={handleNext} />
</CalendarHeader>
{view === "days" && (
<CalendarGrid
weeks={weeks}
visibleMonth={visibleMonth}
value={selectedDate}
onSelect={handleSelect}
firstDayOfWeek={0}
/>
)}
{view === "months" && (
<CalendarMonthGrid
visibleMonth={visibleMonth}
value={selectedDate}
onSelect={handleMonthSelect}
/>
)}
{view === "years" && (
<CalendarYearGrid
visibleMonth={visibleMonth}
value={selectedDate}
onSelect={handleYearSelect}
/>
)}
</CalendarRoot>
);
}
API Reference
High-level Components
Ready-to-use composite component.
Calendar
| Prop | Type | Default | Description |
|---|---|---|---|
visibleMonth | Temporal.PlainDate | - | Required. The month panel currently displayed |
value | Temporal.PlainDate | null | - | Controlled selected date |
defaultValue | Temporal.PlainDate | null | - | Default selected date (uncontrolled) |
view | "days" | "months" | "years" | - | Controlled active panel |
defaultView | "days" | "months" | "years" | "days" | Initial panel (uncontrolled) |
onChange | (date: Temporal.PlainDate) => void | - | Fired when clicking a valid date inside the visible month |
onVisibleMonthChange | (month: Temporal.PlainDate) => void | - | Fired when the visible month changes (previous / next, panel pick, keyboard navigation) |
onViewChange | (view: "days" | "months" | "years") => void | - | Fired when the active panel changes |
isDateDisabled | (date: Temporal.PlainDate) => boolean | - | Return true to disable the date |
firstDayOfWeek | 0 | 1 | 0 | 0 starts the week on Sunday, 1 starts it on Monday |
className | ClassNameValue | - | Custom class names, applied to the root |
Temporal.PlainDate is immutable — all month arithmetic is performed with add / subtract / with, never by mutating an existing object.
Calendar.calendarMonthLabels is process-global — set it once at your app entry (it also affects DatePicker); day-grid weekday abbreviations are fixed English.
Calendar.calendarMonthLabels = ["一月", "二月", /* ... */];Composable Components
CalendarRoot
The panel shell: border, background and vertical spacing.
| Prop | Type | Default | Description |
|---|---|---|---|
className | ClassNameValue | - | Custom class names |
...props | React.ComponentProps<"div"> | - | Native div props |
CalendarHeader
Header shell: a flex row for the navigation buttons and the title buttons.
| Prop | Type | Default | Description |
|---|---|---|---|
className | ClassNameValue | - | Custom class names |
...props | React.ComponentProps<"header"> | - | Native header props |
CalendarNavButton
Chevron navigation button for the header.
| Prop | Type | Default | Description |
|---|---|---|---|
direction | "previous" | "next" | - | Which chevron icon to render |
label | string | "Previous" / "Next" | Accessible name (aria-label) |
className | ClassNameValue | - | Custom class names |
...props | React.ComponentProps<"button"> | - | Native button props (except type and aria-label) |
CalendarTitleButton
Text button for the header title area — typically rendered twice as the year and the month.
| Prop | Type | Default | Description |
|---|---|---|---|
className | ClassNameValue | - | Custom class names |
...props | React.ComponentProps<"button"> | - | Native button props (except type), such as onClick |
CalendarMonthGrid
3 × 4 month panel listing the 12 months of the visible year.
| Prop | Type | Default | Description |
|---|---|---|---|
visibleMonth | Temporal.PlainDate | - | Source of the shown year; its month is the fallback tab stop |
value | Temporal.PlainDate | null | - | Selected date, highlights the cell of the same month |
isMonthDisabled | (month: Temporal.PlainDate) => boolean | - | Receives the first day of each month; return true to disable it |
onSelect | (month: Temporal.PlainDate) => void | - | Fired with the first day of the picked month |
onNavigate | (month: Temporal.PlainDate) => void | - | Fired when arrowing out of the shown year with the target month |
className | ClassNameValue | - | Custom class names |
...props | React.ComponentProps<"div"> | - | Native div props |
CalendarYearGrid
3 × 4 year panel showing a 12-year window centered on the visible year.
| Prop | Type | Default | Description |
|---|---|---|---|
visibleMonth | Temporal.PlainDate | - | Centers the 12-year window; its year is the fallback tab stop |
value | Temporal.PlainDate | null | - | Selected date, highlights the cell of the same year |
isYearDisabled | (year: Temporal.PlainDate) => boolean | - | Receives January 1st of each year; return true to disable it |
onSelect | (year: Temporal.PlainDate) => void | - | Fired with January 1st of the picked year |
onNavigate | (year: Temporal.PlainDate) => void | - | Fired when arrowing out of the window with the target year |
className | ClassNameValue | - | Custom class names |
...props | React.ComponentProps<"div"> | - | Native div props |
CalendarGrid
Weekday header row plus the weeks body — pass a two-dimensional weeks array including previous/next-month padding dates.
| Prop | Type | Default | Description |
|---|---|---|---|
weeks | Temporal.PlainDate[][] | - | Two-dimensional weeks array, 7 dates per row |
visibleMonth | Temporal.PlainDate | - | Used to detect out-of-month cells |
value | Temporal.PlainDate | null | - | Selected date, drives the highlighted cell |
isDateDisabled | (date: Temporal.PlainDate) => boolean | - | Return true to disable the date |
onSelect | (date: Temporal.PlainDate) => void | - | Fired on cell click; the composite filters out-of-month dates |
onNavigate | (date: Temporal.PlainDate) => void | - | Fired when arrowing to a date not present in the grid |
firstDayOfWeek | 0 | 1 | 0 | Weekday header order |
className | ClassNameValue | - | Custom class names |
...props | React.ComponentProps<"div"> | - | Native div props |
CalendarGridRow
A single week row, usually rendered by CalendarGrid.
| Prop | Type | Default | Description |
|---|---|---|---|
week | Temporal.PlainDate[] | - | The 7 dates of this week |
visibleMonth | Temporal.PlainDate | - | Used to detect out-of-month cells |
value | Temporal.PlainDate | null | - | Selected date |
isDateDisabled | (date: Temporal.PlainDate) => boolean | - | Return true to disable the date |
onSelect | (date: Temporal.PlainDate) => void | - | Fired on cell click |
onNavigate | (date: Temporal.PlainDate) => void | - | Fired when arrowing to a date not present in the grid |
tabStopDate | string | - | ISO date string of the single tab stop in the grid; omit to keep all cells natively tabbable |
className | ClassNameValue | - | Custom class names |
...props | React.ComponentProps<"div"> | - | Native div props |
CalendarGridCell
A single date cell rendered as a button.
| Prop | Type | Default | Description |
|---|---|---|---|
date | Temporal.PlainDate | - | The date this cell renders (shows date.day) |
outsideMonth | boolean | false | Grays out cells not belonging to the visible month |
selected | boolean | false | Highlights the cell as selected |
tabStop | boolean | - | Roving tabindex: true makes the cell the only tab stop; omit to keep native tab behavior |
onNavigate | (date: Temporal.PlainDate) => void | - | Fired when arrowing to a date not present in the grid |
className | ClassNameValue | - | Custom class names |
...props | React.ComponentProps<"button"> | - | Native button props (except type), such as disabled |