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/time engine. Month/year navigation and range selection are handled natively by VCP — no separate React header layer.

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

Jun 5, 2026 - Jun 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 exposes the VCP grid mount ref and footer actions so you can rebuild shell/layout while keeping core selection logic.

-- - --

"use client";

import { useState } from "react";
import type { DateRangeValue } from "@/components/f-ui/date-internals/date-value";
import { CalendarBody } from "@/components/f-ui/calendar/calendar-parts/calendar-body";
import { CalendarFooter } from "@/components/f-ui/calendar/calendar-parts/calendar-footer";
import { CalendarShell } from "@/components/f-ui/calendar/calendar-parts/calendar-shell";
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,
  });

  return (
    <CalendarShell>
      <CalendarBody
        gridRefs={calendar.gridRefs}
        visibleMonths={calendar.visibleMonths}
      />
      <CalendarFooter
        mode={calendar.mode}
        showToday={calendar.showToday}
        confirmable={calendar.confirmable}
        rangeSummary={calendar.rangeSummary}
        disabled={calendar.disabled}
        todayLabel={t("today")}
        cancelLabel={t("cancel")}
        applyLabel={t("apply")}
        canApply={calendar.canApply}
        onToday={calendar.onToday}
        onCancel={calendar.onCancel}
        onApply={calendar.onApply}
      />
    </CalendarShell>
  );
}

Composition

Calendar
├── CalendarShell
├── CalendarBody
│   └── CalendarGridMount (VCP grid + native month/year headers)
└── CalendarFooter (Today / Cancel / Apply)

API Reference

Props

PropTypeDefault
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
withTimebooleanfalse
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