f-ui
Components

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.json
npx shadcn@latest add https://ui.isaacfei.com/r/calendar.json
yarn dlx shadcn@latest add https://ui.isaacfei.com/r/calendar.json
bun x shadcn@latest add https://ui.isaacfei.com/r/calendar.json

Or 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}.

Aug 5, 2026 - Aug 16, 2026

"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

PropTypeDefaultDescription
mode"single" | "range"required
valueDate | null or DateRangeValue
onChange(dateOrRange) => void
localestring (BCP 47)i18n/provider locale
minValue / maxValueDate
isDateUnavailable(date: Date) => boolean
firstDayOfWeek0 | 1 | 2 | 3 | 4 | 5 | 6locale default
disabledbooleanfalse
visibleMonths1 | 21
withTimebooleanfalseRenders 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.
confirmablebooleanfalse
showTodaybooleansingle && !confirmable
classNamestring
classNamesPartial<Record<CalendarSlot, string>>
tCalendarTranslateFnbuilt-in/provider

Slots

SlotElement
rootOuter shell
bodyCalendar body wrapper
gridVCP grid mount wrapper
footerFooter actions

Hook - useCalendar(options)

ReturnType
gridRefsRefObject<HTMLDivElement>[]
showToday / confirmable / canApplyboolean
rangeSummarystring | undefined
onToday / onCancel / onApply() => void

On this page