Skip to content

Calendar

A month calendar for picking a date, styled with CK-UI tokens on top of react-day-picker, which provides the date logic and the keyboard behaviour. Use it inline, or through Date Picker in forms.

November 2026

Pick a day with the mouse or keyboard.

Installation

Add Calendar to your project with the CK-UI CLI. It copies the source into your project, so the code is yours to change.

Terminal
npx ck-ui-cli@latest add calendar
Adds
Calendar.tsxto your CK-UI folder (src/components/ck-ui by default)
Installs
react-day-pickerreact-iconsif your project does not have them yet

First time with CK-UI in this project? Run npx ck-ui-cli@latest init once before adding components. The installation guide explains what it sets up.

Manual installationInstall the packages and copy the files yourself.

Install the following dependencies

Run these commands in your terminal to install the required packages

Terminal
npm install react-day-picker react-icons

Create or update your Calendar component

Copy this code to create your Calendar component

/components/ui/Calendar.tsx
"use client";

import { DayPicker, type ChevronProps, type Matcher, type PropsBase, type PropsSingle } from "react-day-picker";
import { RiArrowDownSLine, RiArrowLeftSLine, RiArrowRightSLine, RiArrowUpSLine } from "react-icons/ri";
import { cn } from "@/lib/utils";

/*
 * A month calendar for picking one date, built on react-day-picker (which also does the date arithmetic):
 *   role="grid" with labelled day buttons ("Monday, June 1st, 2026, selected"), one tab stop (roving focus),
 *   arrows move by day/week, PageUp/PageDown by month, Shift+PageUp/PageDown by year, Home/End to week start/end.
 *   The month caption is a polite live region, so month changes are announced.
 * `min` / `max` disable dates outside the range and stop navigation past them.
 * Locale: pass a react-day-picker locale (`import { fr } from "react-day-picker/locale"`); the default is en-US.
 */

export type CalendarProps = Omit<PropsBase, "disabled" | "mode" | "required"> &
    Omit<PropsSingle, "mode"> & {
        /** Earliest selectable date. */
        min?: Date;
        /** Latest selectable date. */
        max?: Date;
        /** Dates that cannot be selected (react-day-picker matchers: dates, ranges, `{ dayOfWeek }`, functions). */
        disabled?: Matcher | Matcher[];
    };

function CalendarChevron({ orientation, className }: ChevronProps) {
    const Icon = orientation === "left" ? RiArrowLeftSLine : orientation === "right" ? RiArrowRightSLine : orientation === "up" ? RiArrowUpSLine : RiArrowDownSLine;
    return <Icon aria-hidden="true" className={cn("size-5", className)} />;
}

const toArray = <T,>(value: T | T[] | undefined): T[] => (value === undefined ? [] : Array.isArray(value) ? value : [value]);

const dayCell = [
    "size-9 p-0 text-center",
    "[&[data-outside]>button]:text-muted-foreground",
    "[&[data-today]>button]:font-semibold [&[data-today]>button]:ring-1 [&[data-today]>button]:ring-inset [&[data-today]>button]:ring-foreground/25",
    "[&[data-selected]>button]:bg-primary [&[data-selected]>button]:text-primary-foreground [&[data-selected]>button]:ring-0 [&[data-selected]>button:hover]:bg-primary-hover",
    "[&[data-disabled]>button]:cursor-not-allowed [&[data-disabled]>button]:text-muted-foreground [&[data-disabled]>button]:line-through [&[data-disabled]>button]:opacity-50 [&[data-disabled]>button:hover]:bg-transparent",
].join(" ");

/** Pass `selected` + `onSelect` (controlled) or let it manage its own selection. */
function Root({ className, classNames, components, showOutsideDays = true, min, max, disabled, startMonth, endMonth, ...props }: CalendarProps) {
    const disabledMatchers = [...toArray(disabled), ...(min ? [{ before: min }] : []), ...(max ? [{ after: max }] : [])];

    return (
        <DayPicker
            mode="single"
            showOutsideDays={showOutsideDays}
            startMonth={startMonth ?? min}
            endMonth={endMonth ?? max}
            disabled={disabledMatchers.length ? disabledMatchers : undefined}
            className={cn("relative w-fit p-3", className)}
            classNames={{
                months: "relative flex flex-col gap-4 sm:flex-row",
                month: "flex w-full flex-col gap-3",
                nav: "absolute inset-x-3 top-3 z-[1] flex items-center justify-between",
                button_previous:
                    "inline-flex size-8 items-center justify-center rounded-lg text-muted-foreground transition-colors duration-fast hover:bg-muted hover:text-foreground focus-ring aria-disabled:pointer-events-none aria-disabled:opacity-40",
                button_next:
                    "inline-flex size-8 items-center justify-center rounded-lg text-muted-foreground transition-colors duration-fast hover:bg-muted hover:text-foreground focus-ring aria-disabled:pointer-events-none aria-disabled:opacity-40",
                month_caption: "flex h-8 items-center justify-center px-10",
                caption_label: "size-label-sm text-foreground",
                month_grid: "w-full border-collapse",
                weekdays: "",
                weekday: "size-9 pb-1 text-center font-normal size-p-xs text-muted-foreground",
                weeks: "",
                week: "",
                day: dayCell,
                day_button:
                    "inline-flex size-9 items-center justify-center rounded-lg size-p-sm text-foreground transition-colors duration-fast hover:bg-muted focus-ring",
                footer: "mt-3 size-p-xs text-muted-foreground",
                hidden: "invisible",
                outside: "",
                today: "",
                selected: "",
                disabled: "",
                focused: "",
                ...classNames,
            }}
            components={{ Chevron: CalendarChevron, ...components }}
            {...props}
        />
    );
}
Root.displayName = "Calendar.Root";

export { Root };

Test Your Component

  • 1. Add the component to a test page
  • 2. Check all props and variants work as expected
  • 3. Verify responsive behavior
  • 4. Test with different content/data
  • 5. Ensure accessibility features work correctly

Disabled Dates

disabled takes dates, ranges, { dayOfWeek }, { before } / { after } and functions. A footer is announced politely.

November 2026
Weekends and public holidays are unavailable.

Min / Max

min and max disable dates outside the range and disable the month buttons past it.

November 2026
Available for the next 14 days.

Locale

Import a locale from react-day-picker/locale: month and day names, labels and the first day of the week follow it. The default is en-US. showOutsideDays={false} hides days from other months.

novembre 2026
November 2026

Accessibility

  • The month is a role="grid" named by its caption ("June 2026"); the caption is a polite live region, so month changes are announced.
  • Each day is a button with a full label ("Tuesday, June 16th, 2026", plus "Today" or "selected"). The selected cell has aria-selected.
  • The grid is one tab stop. Arrows move by day and week, PageUp/PageDown by month, Shift+PageUp/PageDown by year, Home/End to the start and end of the week. Enter or Space selects.
  • Disabled days are announced as unavailable and cannot be selected; the previous/next buttons are disabled at min/max.
  • Today has a ring and bold text, the selected day a filled background — neither relies on color alone.

API Reference

Single-date mode of react-day-picker (other DayPicker props pass through). Import with import * as Calendar from "@/components/ui/Calendar".

Calendar.Root

PropTypeDefaultDescription
selected"Date""undefined"

Selected date (controlled).

onSelect"(date: Date | undefined) => void""undefined"

Called when a day is chosen or unchosen.

month / defaultMonth / onMonthChange"Date""current month"

The displayed month.

min / max"Date""undefined"

Earliest / latest selectable date.

disabled"Matcher | Matcher[]""undefined"

Dates that cannot be selected.

locale"DayPickerLocale""en-US"

From react-day-picker/locale.

weekStartsOn"0 – 6""locale"

First day of the week.

showOutsideDays"boolean""true"

Show days of adjacent months.

today"Date""now"

Which day is marked as today.

footer"ReactNode""undefined"

Text under the grid, announced politely.

autoFocus"boolean""false"

Focus the selected day (or today) on mount.

className"string""undefined"

Additional classes, merged last.

classNames"Partial<ClassNames>""CK-UI styles"

Override classes per part (react-day-picker keys).