avertra-ui
uicalendarsource: ok · hit
Calendar
A calendar component for date selection.
npx shadcn add "https://micomponents.micustomer.io/r/calendar.json"free
Where it goes
calendar
Interaction
dropdown
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Files1 · source cached 10/10/2026, 10:52:30 AM
src/components/ui/calendar.tsx
"use client";
import * as React from "react";
import {
DayButton,
DayPicker,
getDefaultClassNames,
DayPickerProps,
} from "react-day-picker";
import { cn } from "@/stories/lib/utils";
import { Button, buttonVariants } from "@/components/ui/button";
type View = "days" | "months" | "years";
function Calendar({
className,
classNames,
showOutsideDays = true,
buttonVariant = "white",
month: controlledMonth,
onMonthChange,
...props
}: DayPickerProps & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"];
}) {
const defaultClassNames = getDefaultClassNames();
const [month, setMonth] = React.useState<Date>(controlledMonth || new Date());
const actualMonth = controlledMonth || month;
const handleMonthChange = (date: Date) => {
if (onMonthChange) onMonthChange(date);
if (!controlledMonth) setMonth(date);
};
const [view, setView] = React.useState<View>("days");
function onMonthSelect(newMonth: Date) {
handleMonthChange(newMonth);
setView("days");
}
function onYearSelect(newYearDate: Date) {
const newMonthDate = new Date(
newYearDate.getFullYear(),
actualMonth.getMonth(),
1,
);
handleMonthChange(newMonthDate);
setView("months");
}
// function Caption({ displayMonth }: { displayMonth: Date }) {
// return (
// <div className="flex justify-center items-center gap-4 p-2 select-none">
// <Button
// onClick={() => setView("years")}
// className="font-medium hover:underline"
// type="button"
// aria-label="Select year"
// stylebtn={'text'}
// >
// {displayMonth.getFullYear()}
// </Button>
// <Button
// onClick={() => setView("months")}
// className="font-medium hover:underline"
// type="button"
// aria-label="Select month"
// stylebtn={'text'}
// >
// {displayMonth.toLocaleString("default", { month: "long" })}
// </Button>
// </div>
// );
// }
if (view === "months") {
return (
<MonthsGrid year={actualMonth.getFullYear()} onSelect={onMonthSelect} />
);
}
if (view === "years") {
return (
<YearsGrid
selectedYear={actualMonth.getFullYear()}
onSelect={onYearSelect}
/>
);
}
return (
<DayPicker
{...props}
month={actualMonth}
onMonthChange={handleMonthChange}
showOutsideDays={showOutsideDays}
disabled={(date) => date.getMonth() !== actualMonth.getMonth()}
className={cn(
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-background",
className,
)}
captionLayout="label"
components={{
// Caption: Caption,
DayButton: CalendarDayButton,
...props.components,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"flex gap-4 flex-col md:flex-row relative",
defaultClassNames.months,
),
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
nav: cn(
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between px-2",
defaultClassNames.nav,
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-[--cell-size] aria-disabled:opacity-50 p-0 select-none text-center",
defaultClassNames.button_previous,
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-[--cell-size] aria-disabled:opacity-50 p-0 select-none text-center",
defaultClassNames.button_next,
),
month_caption: cn(
"flex items-center justify-center h-[--cell-size] w-full px-[--cell-size] select-none font-medium",
defaultClassNames.month_caption,
),
dropdowns: cn(
"w-full flex items-center text-sm font-medium justify-center h-[--cell-size] gap-1.5",
defaultClassNames.dropdowns,
),
dropdown_root: cn(
"relative border border-input shadow-xs has-focus:ring-[3px] rounded-md",
defaultClassNames.dropdown_root,
),
dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown),
caption_label: cn(
"select-none font-medium",
"text-sm",
defaultClassNames.caption_label,
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground flex-1 text-[0.8rem] select-none",
defaultClassNames.weekday,
),
week: cn("flex w-full mt-2", defaultClassNames.week),
day: cn(
"relative w-full h-full p-0 text-center group/day aspect-square select-none",
defaultClassNames.day,
),
range_start: cn(
"rounded-l-md bg-accent",
defaultClassNames.range_start,
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground font-bold rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today,
),
outside: cn("text-muted-foreground", defaultClassNames.outside),
disabled: cn(
"text-muted-foreground opacity-50 pointer-events-none bg-transparent hover:bg-transparent active:bg-transparent",
defaultClassNames.disabled,
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
/>
);
}
function MonthsGrid({
year,
onSelect,
}: {
year: number;
onSelect: (date: Date) => void;
}) {
const months = Array.from({ length: 12 }, (_, i) => i);
return (
<div className="grid grid-cols-3 gap-3 p-4 w-[300px]">
{months.map((monthIndex) => {
const date = new Date(year, monthIndex, 1);
return (
<Button
key={monthIndex}
className="py-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 text-center"
onClick={() => onSelect(date)}
type="button"
stylebtn={"text"}
>
{date.toLocaleString("default", { month: "long" })}
</Button>
);
})}
</div>
);
}
function YearsGrid({
selectedYear,
onSelect,
}: {
selectedYear: number;
onSelect: (date: Date) => void;
}) {
const startYear = selectedYear - 10;
const years = Array.from({ length: 21 }, (_, i) => startYear + i);
return (
<div className="grid grid-cols-5 gap-3 p-4 w-[300px]">
{years.map((year) => (
<Button
key={year}
className={`py-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 text-center ${
year === selectedYear ? "font-bold underline" : ""
}`}
onClick={() => onSelect(new Date(year, 0, 1))}
type="button"
stylebtn={"text"}
>
{year}
</Button>
))}
</div>
);
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames();
const ref = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus();
}, [modifiers.focused]);
return (
<Button
ref={ref}
variant="black"
stylebtn={"text"}
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"flex aspect-square size-auto w-full min-w-[--cell-size] flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 [&>span]:text-xs [&>span]:opacity-70",
"data-[range-start=true]:rounded-l-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none",
"data-[selected-single=true]:bg-secondary rounded-md data-[selected-single=true]:text-black",
"data-[range-start=true]:bg-secondary data-[range-end=true]:bg-secondary data-[range-middle=true]:bg-muted data-[range-middle=true]:text-accent-foreground",
modifiers.today && "font-bold",
modifiers.disabled &&
" hover:bg-transparent active:bg-transparent disabled:bg-transparent",
darkHoverTextFix(),
defaultClassNames.day,
className,
)}
{...props}
/>
);
}
function darkHoverTextFix() {
return "dark:hover:text-accent-foreground";
}
export { Calendar, CalendarDayButton };