shadimage
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 };

More from avertra-ui

Browse all components in this library →