Eldora UI
Eldora UI
  1. Components
  2. Input
  3. Calendar

Calendar

A component for entering and editing a single date or a range of dates extending shadcn.

MoTuWeThFrSaSu

Selected Date: None

<CalendarHero/>

Installation

Install the following dependencies:

npm install react-day-picker date-fns @remixicon/react

Update the utils helper.

import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}


export const focusInput = [
  // base
  "focus:ring-2",
  // ring color
  "focus:ring-blue-200 focus:dark:ring-blue-700/30",
  // border color
  "focus:border-blue-500 focus:dark:border-blue-700",
]


export const focusRing = [
  // base
  "outline outline-offset-2 outline-0 focus-visible:outline-2",
  // outline color
  "outline-blue-500 dark:outline-blue-500",
]



export const hasErrorInput = [
  // base
  "ring-2",
  // border color
  "border-red-500 dark:border-red-700",
  // ring color
  "ring-red-200 dark:ring-red-700/30",
]

Copy and paste the following code into your project.

"use client"

import * as React from "react"
import {
  RiArrowLeftDoubleLine,
  RiArrowLeftSLine,
  RiArrowRightDoubleLine,
  RiArrowRightSLine,
} from "@remixicon/react"
import { addYears, format, isSameMonth, getMonth, getYear, setMonth, setYear, } from "date-fns"
import {
  DayPicker,
  useDayPicker,
  useDayRender,
  useNavigation,
  type DayPickerRangeProps,
  type DayPickerSingleProps,
  type DayProps,
  type Matcher,
} from "react-day-picker"

import { cn, focusRing } from "@/lib/utils/classes"

interface NavigationButtonProps
  extends React.HTMLAttributes<HTMLButtonElement> {
  onClick: () => void
  icon: React.ElementType
  disabled?: boolean
}

const NavigationButton = React.forwardRef<
  HTMLButtonElement,
  NavigationButtonProps
>(
  (
    { onClick, icon, disabled, ...props }: NavigationButtonProps,
    forwardedRef,
  ) => {
    const Icon = icon
    return (
      <button
        ref={forwardedRef}
        type="button"
        disabled={disabled}
        className={cn(
          "flex size-8 shrink-0 select-none items-center justify-center rounded border p-1 outline-none transition sm:size-[30px]",
          // text color
          "text-gray-600 hover:text-gray-800",
          "dark:text-gray-400 hover:dark:text-gray-200",
          // border color
          "border-gray-300 dark:border-gray-800",
          // background color
          "hover:bg-gray-50 active:bg-gray-100",
          "hover:dark:bg-gray-900 active:dark:bg-gray-800",
          // disabled
          "disabled:pointer-events-none",
          "disabled:border-gray-200 disabled:dark:border-gray-800",
          "disabled:text-gray-400 disabled:dark:text-gray-600",
          focusRing,
        )}
        onClick={onClick}
        {...props}
      >
        <Icon className="size-full shrink-0" />
      </button>
    )
  },
)

NavigationButton.displayName = "NavigationButton"

type OmitKeys<T, K extends keyof T> = {
  [P in keyof T as P extends K ? never : P]: T[P]
}

type KeysToOmit = "showWeekNumber" | "captionLayout" | "mode"

type SingleProps = OmitKeys<DayPickerSingleProps, KeysToOmit>
type RangeProps = OmitKeys<DayPickerRangeProps, KeysToOmit>

type CalendarProps =
  | ({
      mode: "single"
    } & SingleProps)
  | ({
      mode?: undefined
    } & SingleProps)
  | ({
      mode: "range"
    } & RangeProps)

const Calendar = ({
  mode = "single",
  weekStartsOn = 1,
  numberOfMonths = 1,
  enableYearNavigation = false,
  disableNavigation,
  locale,
  className,
  classNames,
  captionLayout = "buttons",
  ...props
}: CalendarProps & { enableYearNavigation?: boolean, captionLayout?: "buttons" | "dropdown-buttons" }) => {
  return (
    <DayPicker
      mode={mode}
      weekStartsOn={weekStartsOn}
      numberOfMonths={numberOfMonths}
      locale={locale}
      showOutsideDays={numberOfMonths === 1 ? true : false}
      className={cn(className)}
      classNames={{
        months: "flex space-y-0",
        month: "space-y-4 p-3",
        nav: "gap-1 flex items-center rounded-full size-full justify-between p-4",
        table: "w-full border-collapse space-y-1",
        head_cell:
          "w-9 font-medium text-sm sm:text-xs text-center text-gray-400 dark:text-gray-600 pb-2",
        row: "w-full mt-0.5",
        cell: cn(
          "relative p-0 text-center focus-within:relative",
          "text-gray-900 dark:text-gray-50",
        ),
        day: cn(
          "size-9 rounded text-sm text-gray-900 dark:text-gray-50",
          "hover:bg-gray-200 hover:dark:bg-gray-700",
          focusRing,
        ),
        day_today: "font-semibold",
        day_selected: cn(
          "rounded",
          "aria-selected:bg-gray-900 aria-selected:text-gray-50",
          "dark:aria-selected:bg-gray-50 dark:aria-selected:text-gray-900",
        ),
        day_disabled:
          "!text-gray-300 dark:!text-gray-700 line-through disabled:hover:bg-transparent",
        day_outside: "text-gray-400 dark:text-gray-600",
        day_range_middle: cn(
          "!rounded-none",
          "aria-selected:!bg-gray-100 aria-selected:!text-gray-900",
          "dark:aria-selected:!bg-gray-900 dark:aria-selected:!text-gray-50",
        ),
        day_range_start: "rounded-r-none !rounded-l",
        day_range_end: "rounded-l-none !rounded-r",
        day_hidden: "invisible",
        ...classNames,
      }}
      components={{
        IconLeft: () => (
          <RiArrowLeftSLine aria-hidden="true" className="size-4" />
        ),
        IconRight: () => (
          <RiArrowRightSLine aria-hidden="true" className="size-4" />
        ),
        Caption: ({ ...props }) => {
          const {
            goToMonth,
            nextMonth,
            previousMonth,
            currentMonth,
            displayMonths,
          } = useNavigation()
          const { numberOfMonths, fromDate, toDate,fromYear,fromMonth,toMonth,toYear } = useDayPicker()

          const displayIndex = displayMonths.findIndex((month) =>
            isSameMonth(props.displayMonth, month),
          )
          const isFirst = displayIndex === 0
          const isLast = displayIndex === displayMonths.length - 1

          const hideNextButton = numberOfMonths > 1 && (isFirst || !isLast)
          const hidePreviousButton = numberOfMonths > 1 && (isLast || !isFirst)

          const goToPreviousYear = () => {
            const targetMonth = addYears(currentMonth, -1)
            if (
              previousMonth &&
              (!fromDate || targetMonth.getTime() >= fromDate.getTime())
            ) {
              goToMonth(targetMonth)
            }
          }

          const goToNextYear = () => {
            const targetMonth = addYears(currentMonth, 1)
            if (
              nextMonth &&
              (!toDate || targetMonth.getTime() <= toDate.getTime())
            ) {
              goToMonth(targetMonth)
            }
          }

          const handleYearChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
            const newYear = parseInt(event.target.value, 10)
            const updatedMonth = setYear(currentMonth, newYear)
            goToMonth(updatedMonth)
          }

          const handleMonthChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
            const newMonth = parseInt(event.target.value, 10)
            const updatedMonth = setMonth(currentMonth, newMonth)
            goToMonth(updatedMonth)
          }

          const monthsList = Array.from({ length: 12 }, (_, i) => ({
            value:i,
            label:format(setMonth(new Date(),i),"MMM")
          }))

          const yearFrom = fromYear || fromMonth?.getFullYear() || fromDate?.getFullYear()
          const lastYear = toYear || toMonth?.getFullYear() || toDate?.getFullYear()

          let selectedItems : {label:string;value:string}[] = []

          if(yearFrom && lastYear){
            const yearLength = lastYear-yearFrom+1
            selectedItems = Array.from({length: yearLength},(_, i) =>({
              label:(yearFrom+i).toString(),
              value:(yearFrom+i).toString()
            }))
          }

          return (
            <div className="flex items-center justify-between">
              {captionLayout === "dropdown-buttons" ? (
                <>
                  <div className="flex items-center gap-1">
                    {enableYearNavigation && !hidePreviousButton && (
                      <NavigationButton
                        disabled={
                          disableNavigation ||
                          !previousMonth ||
                          (fromDate &&
                            addYears(currentMonth, -1).getTime() <
                              fromDate.getTime())
                        }
                        aria-label="Go to previous year"
                        onClick={goToPreviousYear}
                        icon={RiArrowLeftDoubleLine}
                      />
                    )}
                    {!hidePreviousButton && (
                      <NavigationButton
                        disabled={disableNavigation || !previousMonth}
                        aria-label="Go to previous month"
                        onClick={() => previousMonth && goToMonth(previousMonth)}
                        icon={RiArrowLeftSLine}
                      />
                    )}
                  </div>

                  <div className="m-2 flex items-center gap-2">
                    <select
                      value={getMonth(currentMonth)}
                      onChange={handleMonthChange}
                      className="border rounded px-2 py-1 text-sm"
                    >
                      {monthsList.map((month) => (
                        <option key={month.value} value={month.value}>
                          {format(setMonth(new Date(), month.value), "MMMM", { locale })}
                        </option>
                      ))}
                    </select>

                    <select
                      value={getYear(currentMonth)}
                      onChange={handleYearChange}
                      className="border rounded px-2 py-1 text-sm"
                    >
                      {selectedItems.map((year) => (
                        <option key={year.value} value={year.value}>{year.label}</option>
                      ))}
                    </select>
                  </div>

                  <div className="flex items-center gap-1">
                    {!hideNextButton && (
                      <NavigationButton
                        disabled={disableNavigation || !nextMonth}
                        aria-label="Go to next month"
                        onClick={() => nextMonth && goToMonth(nextMonth)}
                        icon={RiArrowRightSLine}
                      />
                    )}
                    {enableYearNavigation && !hideNextButton && (
                      <NavigationButton
                        disabled={
                          disableNavigation ||
                          !nextMonth ||
                          (toDate &&
                            addYears(currentMonth, 1).getTime() > toDate.getTime())
                        }
                        aria-label="Go to next year"
                        onClick={goToNextYear}
                        icon={RiArrowRightDoubleLine}
                      />
                    )}
                  </div>
                </>
              ) : (
                <>
                  <div className="flex items-center gap-1">
                    {enableYearNavigation && !hidePreviousButton && (
                      <NavigationButton
                        disabled={
                          disableNavigation ||
                          !previousMonth ||
                          (fromDate &&
                            addYears(currentMonth, -1).getTime() <
                              fromDate.getTime())
                        }
                        aria-label="Go to previous year"
                        onClick={goToPreviousYear}
                        icon={RiArrowLeftDoubleLine}
                      />
                    )}
                    {!hidePreviousButton && (
                      <NavigationButton
                        disabled={disableNavigation || !previousMonth}
                        aria-label="Go to previous month"
                        onClick={() => previousMonth && goToMonth(previousMonth)}
                        icon={RiArrowLeftSLine}
                      />
                    )}
                  </div>

                  <div
                    role="presentation"
                    aria-live="polite"
                    className="text-sm font-medium capitalize tabular-nums text-gray-900 dark:text-gray-50"
                  >
                    {format(props.displayMonth, "LLLL yyy", { locale })}
                  </div>

                  <div className="flex items-center gap-1">
                    {!hideNextButton && (
                      <NavigationButton
                        disabled={disableNavigation || !nextMonth}
                        aria-label="Go to next month"
                        onClick={() => nextMonth && goToMonth(nextMonth)}
                        icon={RiArrowRightSLine}
                      />
                    )}
                    {enableYearNavigation && !hideNextButton && (
                      <NavigationButton
                        disabled={
                          disableNavigation ||
                          !nextMonth ||
                          (toDate &&
                            addYears(currentMonth, 1).getTime() > toDate.getTime())
                        }
                        aria-label="Go to next year"
                        onClick={goToNextYear}
                        icon={RiArrowRightDoubleLine}
                      />
                    )}
                  </div>
                </>
              )}
            </div>
          )
        },
        Day: ({ date, displayMonth }: DayProps) => {
          const buttonRef = React.useRef<HTMLButtonElement>(null)
          const { activeModifiers, buttonProps, divProps, isButton, isHidden } =
            useDayRender(date, displayMonth, buttonRef)

          const { selected, today, disabled, range_middle } = activeModifiers

          if (isHidden) {
            return <></>
          }

          if (!isButton) {
            return (
              <div
                {...divProps}
                className={cn(
                  "flex items-center justify-center",
                  divProps.className,
                )}
              />
            )
          }

          const {
            children: buttonChildren,
            className: buttonClassName,
            ...buttonPropsRest
          } = buttonProps

          return (
            <button
              ref={buttonRef}
              {...buttonPropsRest}
              type="button"
              className={cn("relative", buttonClassName)}
            >
              {buttonChildren}
              {today && (
                <span
                  className={cn(
                    "absolute inset-x-1/2 bottom-1.5 h-0.5 w-4 -translate-x-1/2 rounded-[2px]",
                    {
                      "bg-blue-500 dark:bg-blue-500": !selected,
                      "!bg-white dark:!bg-gray-950": selected,
                      "!bg-gray-400 dark:!bg-gray-600":
                        selected && range_middle,
                      "bg-gray-400 text-gray-400 dark:bg-gray-400 dark:text-gray-600":
                        disabled,
                    },
                  )}
                />
              )}
            </button>
          )
        },
      }}
      {...(props as SingleProps & RangeProps)}
    />
  )
}

Calendar.displayName = "Calendar"

export { Calendar, type Matcher }

Update the import paths to match your project setup.

With year navigation

MoTuWeThFrSaSu

Selected Date: None

<CalendarYearNavigationExample/>

With disabled navigation

MoTuWeThFrSaSu

Selected Date: None

<CalendarDisableNavigationExample/>

With range and two months

MoTuWeThFrSaSu
MoTuWeThFrSaSu

Selected Range: None

<CalendarRangeExample/>

With Locate

lumamejevesadi

Date sélectionnée: None

<CalendarLocaleExample/>

With No dates

MoTuWeThFrSaSu

Selected date: None

<CalendarToTodayExample/>

With Month and Year Dropdown

MoTuWeThFrSaSu

Selected Range: None

<CalendarWithDropdown/>

Built by karthikmudunuri. The source code is available on GitHub.