Calendar
Preline-styled calendar powered by Vanilla Calendar Pro for single, range, time, and confirmable workflows.
Calendar renders a Preline-inspired shell and uses Vanilla Calendar Pro (VCP) as the grid engine. Month/year navigation and range selection are handled natively by VCP — no separate React header layer. When withTime is set, a scroll time spinner renders below the grid instead of VCP's built-in time row — one column in single mode, two columns in range mode. Single withTime also shows a Done footer button to commit and close explicitly.
Installing
pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/calendar.jsonnpx shadcn@latest add https://ui.isaacfei.com/r/calendar.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/r/calendar.jsonbun x shadcn@latest add https://ui.isaacfei.com/r/calendar.jsonOr with a namespace: npx shadcn@latest add @f-ui/calendar.
Usage
import { Calendar } from "@/components/f-ui/calendar/calendar";
<Calendar
mode="range"
value={range}
onChange={setRange}
visibleMonths={2}
aria-label="Travel dates"
/>;Examples
Single Date
Basic calendar with a Today footer action.
"use client";
import { useState } from "react";
import { Calendar } from "@/components/f-ui/calendar/calendar";
export function CalendarDemo() {
const [date, setDate] = useState<Date | undefined>(new Date());
return (
<div className="max-w-sm">
<Calendar
mode="single"
value={date ?? null}
onChange={setDate}
showToday
aria-label="Booking date"
/>
</div>
);
}Two-Month Range
Preline-style dual panel range calendar.
"use client";
import { useState } from "react";
import { Calendar } from "@/components/f-ui/calendar/calendar";
import type { DateRangeValue } from "@/components/f-ui/date-internals/date-value";
export function CalendarRangeDemo() {
const [range, setRange] = useState<DateRangeValue | undefined>();
return (
<div className="max-w-xl w-full">
<Calendar
mode="range"
value={range}
onChange={setRange}
visibleMonths={2}
showToday
aria-label="Trip dates"
/>
</div>
);
}Single Panel Range
Range mode with visibleMonths={1}.
"use client";
import { useMemo, useState } from "react";
import { Calendar } from "@/components/f-ui/calendar/calendar";
import type { DateRangeValue } from "@/components/f-ui/date-internals/date-value";
export function CalendarRangeSinglePaneDemo() {
const initial = useMemo(() => {
const now = new Date();
return {
from: new Date(now.getFullYear(), now.getMonth(), 5),
to: new Date(now.getFullYear(), now.getMonth(), 16),
} satisfies DateRangeValue;
}, []);
const [range, setRange] = useState<DateRangeValue | undefined>(initial);
return (
<div className="max-w-xl w-full">
<Calendar
mode="range"
value={range}
onChange={setRange}
visibleMonths={1}
showToday
aria-label="Booking window — single-pane range"
/>
</div>
);
}Multi-Month Single Mode
Three visible months for quick scanning.
"use client";
import { useState } from "react";
import { Calendar } from "@/components/f-ui/calendar/calendar";
export function CalendarMultiMonthDemo() {
const [date, setDate] = useState<Date | undefined>(new Date());
return (
<div className="max-w-full overflow-x-auto pb-1">
<Calendar
mode="single"
visibleMonths={2}
value={date ?? null}
onChange={setDate}
showToday
className="min-w-fit"
aria-label="Multi-month picker"
/>
</div>
);
}Header Controls
VCP native month/year pickers with minValue/maxValue.
"use client";
import { useState } from "react";
import { Calendar } from "@/components/f-ui/calendar/calendar";
const minDate = new Date(new Date().getFullYear() - 1, 0, 1);
const maxDate = new Date(new Date().getFullYear() + 1, 11, 31);
export function CalendarHeaderControlsDemo() {
const [date, setDate] = useState<Date | undefined>();
return (
<div className="max-w-sm">
<Calendar
mode="single"
value={date ?? null}
onChange={setDate}
minValue={minDate}
maxValue={maxDate}
aria-label="Fiscal date"
/>
</div>
);
}Headless Usage
useCalendar is the view-model: VCP grid refs, range summary, and footer actions. The demo mounts bare <div ref={gridRefs[i]} /> nodes and native <button> elements — not CalendarShell, CalendarBody, or CalendarFooter.
-- - --
value: —
"use client";
import { useState } from "react";
import type { DateRangeValue } from "@/components/f-ui/date-internals/date-value";
import { useCalendarI18n } from "@/components/f-ui/calendar/hooks/use-calendar-i18n";
import { useCalendar } from "@/components/f-ui/calendar/use-calendar";
export function CalendarHeadlessDemo() {
const [range, setRange] = useState<DateRangeValue | undefined>();
const { t, locale } = useCalendarI18n();
const calendar = useCalendar({
mode: "range",
value: range,
onChange: setRange,
locale,
t,
visibleMonths: 2,
confirmable: true,
showToday: false,
});
const valueLabel =
range?.from && range?.to
? `${range.from.toLocaleDateString(locale)} – ${range.to.toLocaleDateString(locale)}`
: "—";
return (
<div>
<div ref={calendar.gridRefs[0]} />
{calendar.visibleMonths === 2 && calendar.gridRefs[1] ? (
<div ref={calendar.gridRefs[1]} />
) : null}
{calendar.rangeSummary ? <p>{calendar.rangeSummary}</p> : null}
{calendar.confirmable ? (
<div>
<button type="button" onClick={calendar.onCancel}>
{t("cancel")}
</button>
<button
type="button"
onClick={calendar.onApply}
disabled={!calendar.canApply}
>
{t("apply")}
</button>
</div>
) : null}
<p>value: {valueLabel}</p>
</div>
);
}Composition
Calendar
├── CalendarShell
├── CalendarBody
│ └── CalendarGridMount (VCP grid + native month/year headers)
└── CalendarFooter (Today / Cancel / Apply)API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
mode | "single" | "range" | required | — |
value | Date | null or DateRangeValue | — | — |
onChange | (dateOrRange) => void | — | — |
locale | string (BCP 47) | i18n/provider locale | — |
minValue / maxValue | Date | — | — |
isDateUnavailable | (date: Date) => boolean | — | — |
firstDayOfWeek | 0 | 1 | 2 | 3 | 4 | 5 | 6 | locale default | — |
disabled | boolean | false | — |
visibleMonths | 1 | 2 | 1 | — |
withTime | boolean | false | Renders a scroll time spinner below the grid instead of VCP's built-in time row — one column in single mode, two in range mode. |
timeOptions | { format?: string; use12Hours?: boolean; step?: { hour?; minute?; second? }; density?: "default" | "compact" } | — | Configures the in-popover time spinner when withTime is set. density defaults to "compact"; pass "default" for full height. Also supports 12-hour, seconds, and stepped columns. |
confirmable | boolean | false | — |
showToday | boolean | single && !confirmable | — |
className | string | — | — |
classNames | Partial<Record<CalendarSlot, string>> | — | — |
t | CalendarTranslateFn | built-in/provider | — |
Slots
| Slot | Element |
|---|---|
root | Outer shell |
body | Calendar body wrapper |
grid | VCP grid mount wrapper |
footer | Footer actions |
Hook - useCalendar(options)
| Return | Type |
|---|---|
gridRefs | RefObject<HTMLDivElement>[] |
showToday / confirmable / canApply | boolean |
rangeSummary | string | undefined |
onToday / onCancel / onApply | () => void |