Calendar
A calendar displays one or more date grids and allows users to select either a single date or a contiguous range of dates.
July 2025
S | M | T | W | T | F | S |
---|---|---|---|---|---|---|
29 | 30 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | 1 | 2 |
Range Calendar
July 2025
S | M | T | W | T | F | S |
---|---|---|---|---|---|---|
29 | 30 | 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | 1 | 2 |
Source
"use client";
import {
Calendar as AriaCalendar,
type CalendarProps as AriaCalendarProps,
RangeCalendar as AriaRangeCalendar,
type RangeCalendarProps as AriaRangeCalendarProps,
Button,
CalendarCell,
CalendarGrid,
CalendarGridBody,
CalendarGridHeader,
CalendarHeaderCell,
type DateValue,
Heading,
Text,
} from "react-aria-components";
import { tv } from "tailwind-variants";
import { ChevronLeft, ChevronRight } from "lucide-react";
const baseStyles = tv({
slots: {
root: "w-fit max-w-full rounded-2xl border border-border bg-surface p-4 text-fg",
header: "flex w-full items-center gap-1 pb-4",
heading: "flex-1 text-center font-bold",
headerCell: "pb-2 text-fg-muted text-sm",
monthButton:
"flex appearance-none items-center justify-center rounded-full p-2 text-center outline-none ring-focus data-[hovered]:bg-secondary data-[focus-visible]:ring-2",
},
});
const calendar = tv({
extend: baseStyles,
slots: {
cell: "flex size-9 cursor-default items-center justify-center rounded-full border-border text-center text-sm outline-focus outline-offset-2 data-[hovered]:bg-secondary data-[pressed]:bg-secondary data-[selected]:bg-primary data-[selected]:text-primary-fg data-[unavailable]:text-fg-muted data-[unavailable]:line-through data-[focus-visible]:outline-2 data-[focus-visible]:outline-focus [&[data-outside-month]]:hidden",
},
});
const rangeCalendar = tv({
extend: baseStyles,
slots: {
cell: "flex size-9 cursor-default items-center justify-center rounded-full text-center text-sm outline-none outline-offset-2 data-[selected]:rounded-none data-[hovered]:bg-secondary data-[pressed]:bg-secondary data-[selected]:bg-primary data-[selected]:text-primary-fg data-[unavailable]:text-fg-muted data-[unavailable]:line-through data-[focus-visible]:ring-2 data-[focus-visible]:ring-focus data-[focus-visible]:ring-offset-2 [&[data-outside-month]]:hidden [&[data-selection-end]]:rounded-r-full [&[data-selection-start]]:rounded-l-full",
},
});
const styles = calendar();
const rangeStyles = rangeCalendar();
interface CalendarProps<T extends DateValue>
extends Omit<AriaCalendarProps<T>, "className"> {
errorMessage?: string;
className?: string;
}
const Calendar = ({
className,
errorMessage,
...props
}: CalendarProps<DateValue>) => (
<AriaCalendar {...props} className={styles.root({ className })}>
<header className={styles.header()}>
<Button className={styles.monthButton()} slot="previous">
<ChevronLeft className="h-5 w-5 self-center" />
</Button>
<Heading className={styles.heading()} slot="label" />
<Button className={styles.monthButton()} slot="next">
<ChevronRight className="h-5 w-5 self-center" />
</Button>
</header>
<CalendarGrid>
<CalendarGridHeader>
{(day) => (
<CalendarHeaderCell className={styles.headerCell()}>
{day}
</CalendarHeaderCell>
)}
</CalendarGridHeader>
<CalendarGridBody>
{(date) => <CalendarCell className={styles.cell()} date={date} />}
</CalendarGridBody>
</CalendarGrid>
{errorMessage && (
<Text className="text-danger text-sm" slot="errorMessage">
{errorMessage}
</Text>
)}
</AriaCalendar>
);
interface RangeCalendarProps<T extends DateValue>
extends Omit<AriaRangeCalendarProps<T>, "className"> {
errorMessage?: string;
className?: string;
}
const RangeCalendar = ({
className,
errorMessage,
...props
}: RangeCalendarProps<DateValue>) => (
<AriaRangeCalendar {...props} className={rangeStyles.root({ className })}>
<header className={rangeStyles.header()}>
<Button className={rangeStyles.monthButton()} slot="previous">
<ChevronLeft className="h-5 w-5 self-center" />
</Button>
<Heading className={rangeStyles.heading()} slot="label" />
<Button className={rangeStyles.monthButton()} slot="next">
<ChevronRight className="h-5 w-5 self-center" />
</Button>
</header>
<CalendarGrid>
<CalendarGridHeader>
{(day) => (
<CalendarHeaderCell className={rangeStyles.headerCell()}>
{day}
</CalendarHeaderCell>
)}
</CalendarGridHeader>
<CalendarGridBody>
{(date) => <CalendarCell className={rangeStyles.cell()} date={date} />}
</CalendarGridBody>
</CalendarGrid>
{errorMessage && (
<Text className="text-danger text-sm" slot="errorMessage">
{errorMessage}
</Text>
)}
</AriaRangeCalendar>
);
export { Calendar, RangeCalendar };
export type { CalendarProps, RangeCalendarProps };