Get started

Install dependencies:

npm i react-composable-calendar dayjs

Copy paste into your codebase

This package is designed with composability and headless components at its core, following the same principles
as shadcn/ui and Radix UI. It offers flexibility and customization, empowering you to build exactly what you need.

Explore the examples below, find the one that aligns with your vision, and start crafting your perfect calendar component with ease.

The examples require you to have shadcn/ui setup in your project.

Change locale

To change locale of the calendar, you can pass a locale to the root component or set the locale for dayjs globally.

import * as Calendar from "react-composable-calendar";
import "dayjs/locale/en-gb.js";

export function Component() {
  return (
    <Calendar.Root mode="single" locale="en-gb">
      {/* ... */}
    </Calendar.Root>
  );
}
import dayjs from "dayjs";
import "dayjs/locale/en-gb.js";

dayjs.locale("en-gb");
Basic Calendar
A simple calendar that accepts a single date or a range.

Installation

    Setup shadcn/ui.
    Add Button.
    Copy paste this code block into src/components/ui/calendar.tsx.
"use client";

import { cn } from "@/lib/utils.ts";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import * as Calendar from "react-composable-calendar";
import { Button } from "./button.tsx";

export function CalendarBody() {
  return (
    <Calendar.View>
      <div className="mb-4 flex items-center justify-between">
        <Calendar.OffsetViewButton asChild offset={-1}>
          <Button size="icon" variant="outline" className="size-8">
            <ChevronLeftIcon className="size-3" />
          </Button>
        </Calendar.OffsetViewButton>
        <Calendar.MonthTitle className="flex items-center justify-center text-sm" />
        <Calendar.OffsetViewButton asChild offset={1}>
          <Button size="icon" variant="outline" className="size-8">
            <ChevronRightIcon className="size-3" />
          </Button>
        </Calendar.OffsetViewButton>
      </div>
      <Calendar.Weekdays className="mb-3 grid grid-cols-7 font-light text-muted-foreground text-xs">
        <Calendar.WeekdayLabel className="flex items-center justify-center" />
      </Calendar.Weekdays>
      <Calendar.Days className="mb-1 grid grid-cols-7 gap-y-1">
        <Calendar.Day className="group relative aspect-square w-full cursor-pointer">
          <Calendar.DayInRange className="absolute top-0 right-0 bottom-0 left-0 bg-foreground/10 data-end:rounded-r-lg data-start:rounded-l-lg" />
          <div className="absolute top-0 right-0 bottom-0 left-0 z-20 flex items-center justify-center rounded-lg group-data-[is-today]:bg-muted group-data-[selected]:bg-foreground">
            <Calendar.DayLabel className="group-data-[neighboring]:text-muted-foreground group-data-[selected]:text-background" />
          </div>
        </Calendar.Day>
      </Calendar.Days>
      <Calendar.FormInput />
    </Calendar.View>
  );
}

export function BasicCalendar(props: Calendar.RootProps) {
  const { className, ...rest } = props;

  return (
    <Calendar.Root className={cn("max-w-72 p-3", className)} {...rest}>
      <CalendarBody />
    </Calendar.Root>
  );
}
Date Picker
A Date Picker input that utilizes shadcn/ui's Button and Popover.

Installation

"use client";

import { Button } from "@/components/ui/button";
import { CalendarBody } from "@/components/ui/calendar";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { Calendar as CalendarIcon } from "lucide-react";
import type { ComponentProps } from "react";
import * as Calendar from "react-composable-calendar";
import { useHasValue } from "react-composable-calendar/hooks";

export type DatePickerTriggerProps = ComponentProps<typeof Button>;

export function DatePickerTrigger(props: DatePickerTriggerProps) {
  const { className, ...rest } = props;

  const hasValue = useHasValue();

  return (
    <Button
      variant="outline"
      className={cn(
        "w-[280px] justify-start text-left font-normal",
        !hasValue && "text-muted-foreground",
        className,
      )}
      {...rest}
    >
      <CalendarIcon className="mr-2 h-4 w-4" />
      <Calendar.ValueLabel fallback="Select a date" />
    </Button>
  );
}

export type DatePickerProps = Calendar.RootProps;

export function DatePicker(props: DatePickerProps) {
  const { children, ...rest } = props;
  return (
    <Calendar.Root {...rest}>
      <Popover>
        <PopoverTrigger asChild>
          <DatePickerTrigger />
        </PopoverTrigger>
        <PopoverContent className="w-auto min-w-72 p-3">
          <CalendarBody />
        </PopoverContent>
      </Popover>
    </Calendar.Root>
  );
}
Multiple Views
You can have multiple views under the same calendar root.

Installation

    Setup shadcn/ui.
    Add Button.
    Copy paste this code block into src/components/ui/calendar.tsx.
"use client";

import { cn } from "@/lib/utils.ts";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import * as Calendar from "react-composable-calendar";
import { Button } from "./button.tsx";

export function range(length: number) {
  return [...new Array(length)].map((_, i) => i);
}

export function MultiViewCalendar(props: Calendar.RootProps) {
  const { className, ...rest } = props;

  return (
    <Calendar.Root className={cn("max-w-lg p-3", className)} {...rest}>
      <Calendar.View>
        <div className="mb-2 flex items-center justify-end gap-2">
          <Calendar.OffsetViewButton asChild offset={-1}>
            <Button size="icon" variant="outline" className="size-8">
              <ChevronLeftIcon className="size-3" />
            </Button>
          </Calendar.OffsetViewButton>
          <Calendar.OffsetViewButton asChild offset={1}>
            <Button size="icon" variant="outline" className="size-8">
              <ChevronRightIcon className="size-3" />
            </Button>
          </Calendar.OffsetViewButton>
          <div className="grow" />
          <Calendar.ValueLabel className="text-muted-foreground text-sm " />
        </div>

        <div className="grid grid-cols-2 gap-6">
          {range(2).map((index) => (
            <Calendar.ViewOffset offset={index} key={index}>
              <Calendar.MonthTitle className="mb-4 flex items-center justify-center" />
              <Calendar.Weekdays className="mb-2 grid grid-cols-7 font-light text-muted-foreground text-xs">
                <Calendar.WeekdayLabel className="flex items-center justify-center" />
              </Calendar.Weekdays>

              <Calendar.Days className="mb-1 grid grid-cols-7 gap-y-1">
                <Calendar.Day className="group relative aspect-square w-full cursor-pointer data-[neighboring]:invisible ">
                  <Calendar.DayInRange className="absolute top-0 right-0 bottom-0 left-0 bg-foreground/10 data-end:rounded-r-lg data-start:rounded-l-lg" />
                  <div className="absolute top-0 right-0 bottom-0 left-0 z-20 flex items-center justify-center rounded-lg group-data-[is-today]:bg-muted group-data-[selected]:bg-foreground">
                    <Calendar.DayLabel className="group-data-[neighboring]:text-muted-foreground group-data-[selected]:text-background" />
                  </div>
                </Calendar.Day>
              </Calendar.Days>
            </Calendar.ViewOffset>
          ))}
        </div>
        <Calendar.FormInput />
      </Calendar.View>
    </Calendar.Root>
  );
}
Select start and end date separately
You can even have two views that work independently of each other.

Installation

    Setup shadcn/ui.
    Add Button.
    Copy paste this code block into src/components/ui/calendar.tsx.
"use client";

import { cn } from "@/lib/utils.ts";
import dayjs from "dayjs";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import * as Calendar from "react-composable-calendar";
import { changeAtIndexStrategy } from "react-composable-calendar/select-day-strategy";
import { Button } from "./button.tsx";

export function range(length: number) {
  return [...new Array(length)].map((_, i) => i);
}

export function CalendarStartEndSeparate(props: Calendar.RootProps) {
  const { className, ...rest } = props;

  return (
    <Calendar.Root className={cn("max-w-lg p-3", className)} {...rest}>
      <Calendar.ValueLabel
        fallback="No date selected"
        className="mb-3 text-center text-muted-foreground text-sm"
      />

      <div className="grid grid-cols-2 gap-6">
        {range(2).map((index) => (
          <Calendar.View defaultValue={dayjs().add(index, "month")} key={index}>
            <div className="mb-4 flex items-center justify-between">
              <Calendar.OffsetViewButton asChild offset={-1}>
                <Button size="icon" variant="outline" className="size-8">
                  <ChevronLeftIcon className="size-3" />
                </Button>
              </Calendar.OffsetViewButton>
              <Calendar.MonthTitle className="flex items-center justify-center" />
              <Calendar.OffsetViewButton asChild offset={1}>
                <Button size="icon" variant="outline" className="size-8">
                  <ChevronRightIcon className="size-3" />
                </Button>
              </Calendar.OffsetViewButton>
            </div>
            <Calendar.Weekdays className="mb-2 grid grid-cols-7 font-light text-muted-foreground text-xs">
              <Calendar.WeekdayLabel className="flex items-center justify-center" />
            </Calendar.Weekdays>

            <Calendar.Days className="mb-1 grid grid-cols-7 gap-y-1">
              <Calendar.Day
                selectDayStrategy={changeAtIndexStrategy(index)}
                className="group relative aspect-square w-full cursor-pointer data-[neighboring]:invisible"
              >
                <Calendar.DayInRange className="absolute top-0 right-0 bottom-0 left-0 bg-foreground/10 data-end:rounded-r-lg data-start:rounded-l-lg" />
                <div className="absolute top-0 right-0 bottom-0 left-0 z-20 flex items-center justify-center rounded-lg group-data-[is-today]:bg-muted group-data-[selected]:bg-foreground">
                  <Calendar.DayLabel className="group-data-[neighboring]:text-muted-foreground group-data-[selected]:text-background" />
                </div>
              </Calendar.Day>
            </Calendar.Days>
          </Calendar.View>
        ))}
      </div>
      <Calendar.FormInput />
    </Calendar.Root>
  );
}
With Year Select
If you need a wide range of dates, you can add a dropdown to select year.

Installation

    Setup shadcn/ui.
    Add Button.
    Add Select.
    Copy paste this code block into src/components/ui/calendar.tsx.
"use client";

import { cn } from "@/lib/utils.ts";
import dayjs from "dayjs";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import * as Calendar from "react-composable-calendar";
import { useViewState } from "react-composable-calendar/hooks";
import { Button } from "./button.tsx";
import { Select, SelectContent, SelectItem, SelectTrigger } from "./select.tsx";

export function range(start: number, end: number) {
  const length = end - start + 1;
  return [...new Array(length)].map((_, i) => i + start);
}

const yearRange = range(
  dayjs().add(-30, "years").get("year"),
  dayjs().add(30, "years").get("year"),
);

function YearSelect() {
  const [view, setView] = useViewState();

  return (
    <Select
      value={view.get("year").toString()}
      onValueChange={(year) => setView(view.set("year", Number(year)))}
    >
      <SelectTrigger className="w-auto">
        <Calendar.MonthTitle className="mr-2 flex items-center justify-center text-sm" />
      </SelectTrigger>
      <SelectContent>
        {yearRange.map((year) => (
          <SelectItem key={year} value={year.toString()}>
            {year}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  );
}

export function CalendarWithYearSelect(props: Calendar.RootProps) {
  const { className, ...rest } = props;

  return (
    <Calendar.Root className={cn("max-w-72 p-3", className)} {...rest}>
      <Calendar.View>
        <div className="mb-4 flex items-center justify-between">
          <Calendar.OffsetViewButton asChild offset={-1}>
            <Button size="icon" variant="outline" className="size-8">
              <ChevronLeftIcon className="size-3" />
            </Button>
          </Calendar.OffsetViewButton>
          <YearSelect />
          <Calendar.OffsetViewButton asChild offset={1}>
            <Button size="icon" variant="outline" className="size-8">
              <ChevronRightIcon className="size-3" />
            </Button>
          </Calendar.OffsetViewButton>
        </div>
        <Calendar.Weekdays className="mb-3 grid grid-cols-7 font-light text-muted-foreground text-xs">
          <Calendar.WeekdayLabel className="flex items-center justify-center" />
        </Calendar.Weekdays>
        <Calendar.Days className="mb-1 grid grid-cols-7 gap-y-1">
          <Calendar.Day className="group relative aspect-square w-full cursor-pointer">
            <Calendar.DayInRange className="absolute top-0 right-0 bottom-0 left-0 bg-foreground/10 data-end:rounded-r-lg data-start:rounded-l-lg" />
            <div className="absolute top-0 right-0 bottom-0 left-0 z-20 flex items-center justify-center rounded-lg group-data-[is-today]:bg-muted group-data-[selected]:bg-foreground">
              <Calendar.DayLabel className="group-data-[neighboring]:text-muted-foreground group-data-[selected]:text-background" />
            </div>
          </Calendar.Day>
        </Calendar.Days>
        <Calendar.FormInput />
      </Calendar.View>
    </Calendar.Root>
  );
}
Disabled Dates
You can disable dates in the view.

Installation

    Setup shadcn/ui.
    Add Button.
    Copy paste this code block into src/components/ui/calendar.tsx.
"use client";

import { cn } from "@/lib/utils.ts";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import * as Calendar from "react-composable-calendar";
import { Button } from "./button.tsx";

export function CalendarDisabledDates(props: Calendar.RootProps) {
  const { className, ...rest } = props;

  return (
    <Calendar.Root className={cn("max-w-72 p-3", className)} {...rest}>
      <Calendar.View
        isDateSelectableFn={(date) => {
          return date.day() !== 0 && date.day() !== 6;
        }}
      >
        <div className="mb-4 flex items-center justify-between">
          <Calendar.OffsetViewButton asChild offset={-1}>
            <Button size="icon" variant="outline" className="size-8">
              <ChevronLeftIcon className="size-3" />
            </Button>
          </Calendar.OffsetViewButton>
          <Calendar.MonthTitle className="flex items-center justify-center text-sm" />
          <Calendar.OffsetViewButton asChild offset={1}>
            <Button size="icon" variant="outline" className="size-8">
              <ChevronRightIcon className="size-3" />
            </Button>
          </Calendar.OffsetViewButton>
        </div>
        <Calendar.Weekdays className="mb-3 grid grid-cols-7 font-light text-muted-foreground text-xs">
          <Calendar.WeekdayLabel className="flex items-center justify-center" />
        </Calendar.Weekdays>
        <Calendar.Days className="mb-1 grid grid-cols-7 gap-y-1">
          <Calendar.Day className="group relative aspect-square w-full cursor-pointer ">
            <Calendar.DayInRange className="absolute top-0 right-0 bottom-0 left-0 bg-foreground/10 data-end:rounded-r-lg data-start:rounded-l-lg" />
            <div className="absolute top-0 right-0 bottom-0 left-0 z-20 flex items-center justify-center rounded-lg group-data-[neighboring]:invisible group-data-[is-today]:bg-muted group-data-[selected]:bg-foreground">
              <Calendar.DayLabel className="group-disabled:text-muted-foreground/50 group-data-[selected]:text-background" />
            </div>
          </Calendar.Day>
        </Calendar.Days>
        <Calendar.FormInput />
      </Calendar.View>
    </Calendar.Root>
  );
}