Install dependencies:
npm i react-composable-calendar dayjs
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.
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");
"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>
);
}
src/components/ui/date-picker.tsx
"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>
);
}
"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>
);
}
"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>
);
}
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>
);
}
"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>
);
}