Beta

Inputs

Date Picker

A component for selecting a date.

Selected Date: None

Installation

  1. 1

    Add required components:

    Please add the Button and Calendar components to use the DatePicker.
  2. 2

    Install dependencies:

    npm install react-day-picker date-fns @remixicon/react @radix-ui/react-popover tailwind-variants
  3. 3

    Add component:

    Copy and paste the code into your project’s component directory. Do not forget to update the import paths. Note that the DatePicker.tsx file includes both the "single" and "range" pickers because they share logic.
    // Tremor Raw Date Picker [v1.0.1]
    "use client"
    import * as React from "react"import { Time } from "@internationalized/date"import * as PopoverPrimitives from "@radix-ui/react-popover"import {  AriaTimeFieldProps,  TimeValue,  useDateSegment,  useTimeField,} from "@react-aria/datepicker"import {  useTimeFieldState,  type DateFieldState,  type DateSegment,} from "@react-stately/datepicker"import { RiCalendar2Fill, RiSubtractFill } from "@remixicon/react"import { format, type Locale } from "date-fns"import { enUS } from "date-fns/locale"import { tv, VariantProps } from "tailwind-variants"
    import { cx, focusInput, focusRing, hasErrorInput } from "@/lib/utils"
    import { Button } from "./Button"import {  Calendar,  Calendar as CalendarPrimitive,  type Matcher,} from "./Calendar"
    //#region TimeInput// ============================================================================
    const isBrowserLocaleClockType24h = () => {  const language =    typeof window !== "undefined" ? window.navigator.language : "en-US"
      const hr = new Intl.DateTimeFormat(language, {    hour: "numeric",  }).format()
      return Number.isInteger(Number(hr))}
    type TimeSegmentProps = {  segment: DateSegment  state: DateFieldState}
    const TimeSegment = ({ segment, state }: TimeSegmentProps) => {  const ref = React.useRef<HTMLDivElement>(null)
      const { segmentProps } = useDateSegment(segment, state, ref)
      const isColon = segment.type === "literal" && segment.text === ":"  const isSpace = segment.type === "literal" && segment.text === " "
      const isDecorator = isColon || isSpace
      return (    <div      {...segmentProps}      ref={ref}      className={cx(        // base        "relative block w-full appearance-none rounded-md border px-2.5 py-1.5 text-left uppercase tabular-nums shadow-sm outline-none transition sm:text-sm",        // border color        "border-gray-300 dark:border-gray-800",        // text color        "text-gray-900 dark:text-gray-50",        // background color        "bg-white dark:bg-gray-950",        // focus        focusInput,        // invalid (optional)        "invalid:border-red-500 invalid:ring-2 invalid:ring-red-200 group-aria-[invalid=true]/time-input:border-red-500 group-aria-[invalid=true]/time-input:ring-2 group-aria-[invalid=true]/time-input:ring-red-200 group-aria-[invalid=true]/time-input:dark:ring-red-400/20",        {          "!w-fit border-none bg-transparent px-0 text-gray-400 shadow-none":            isDecorator,          hidden: isSpace,          "border-gray-300 bg-gray-100 text-gray-400 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-500":            state.isDisabled,          "!bg-transparent !text-gray-400": !segment.isEditable,        },      )}    >      <span        aria-hidden="true"        className={cx(          "pointer-events-none block w-full text-left text-gray-700 sm:text-sm",          {            hidden: !segment.isPlaceholder,            "h-0": !segment.isPlaceholder,          },        )}      >        {segment.placeholder}      </span>      {segment.isPlaceholder ? "" : segment.text}    </div>  )}
    type TimeInputProps = Omit<  AriaTimeFieldProps<TimeValue>,  "label" | "shouldForceLeadingZeros" | "description" | "errorMessage">
    const TimeInput = React.forwardRef<HTMLDivElement, TimeInputProps>(  ({ hourCycle, ...props }: TimeInputProps, ref) => {    const innerRef = React.useRef<HTMLDivElement>(null)
        React.useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(      ref,      () => innerRef?.current,    )
        const locale = window !== undefined ? window.navigator.language : "en-US"
        const state = useTimeFieldState({      hourCycle: hourCycle,      locale: locale,      shouldForceLeadingZeros: true,      autoFocus: true,      ...props,    })
        const { fieldProps } = useTimeField(      {        ...props,        hourCycle: hourCycle,        shouldForceLeadingZeros: true,      },      state,      innerRef,    )
        return (      <div        {...fieldProps}        ref={innerRef}        className="group/time-input inline-flex w-full gap-x-2"      >        {state.segments.map((segment, i) => (          <TimeSegment key={i} segment={segment} state={state} />        ))}      </div>    )  },)TimeInput.displayName = "TimeInput"
    //#region Trigger// ============================================================================
    const triggerStyles = tv({  base: [    // base    "peer flex w-full cursor-pointer appearance-none items-center gap-x-2 truncate rounded-md border px-3 py-2 shadow-sm outline-none transition-all sm:text-sm",    // background color    "bg-white dark:bg-gray-950 ",    // border color    "border-gray-300 dark:border-gray-800",    // text color    "text-gray-900 dark:text-gray-50",    // placeholder color    "placeholder-gray-400 dark:placeholder-gray-500",    // hover    "hover:bg-gray-50 hover:dark:bg-gray-950/50",    // disabled    "disabled:pointer-events-none",    "disabled:bg-gray-100 disabled:text-gray-400",    "disabled:dark:border-gray-800 disabled:dark:bg-gray-800 disabled:dark:text-gray-500",    // focus    focusInput,    // invalid (optional)    // "aria-[invalid=true]:dark:ring-red-400/20 aria-[invalid=true]:ring-2 aria-[invalid=true]:ring-red-200 aria-[invalid=true]:border-red-500 invalid:ring-2 invalid:ring-red-200 invalid:border-red-500"  ],  variants: {    hasError: {      true: hasErrorInput,    },  },})
    interface TriggerProps  extends React.ComponentProps<"button">,    VariantProps<typeof triggerStyles> {  placeholder?: string}
    const Trigger = React.forwardRef<HTMLButtonElement, TriggerProps>(  (    { className, children, placeholder, hasError, ...props }: TriggerProps,    forwardedRef,  ) => {    return (      <PopoverPrimitives.Trigger asChild>        <button          ref={forwardedRef}          className={cx(triggerStyles({ hasError }), className)}          {...props}        >          <RiCalendar2Fill className="size-5 shrink-0 text-gray-400 dark:text-gray-600" />          <span className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-left text-gray-900 dark:text-gray-50">            {children ? (              children            ) : placeholder ? (              <span className="text-gray-400 dark:text-gray-600">                {placeholder}              </span>            ) : null}          </span>        </button>      </PopoverPrimitives.Trigger>    )  },)
    Trigger.displayName = "DatePicker.Trigger"
    //#region Popover// ============================================================================
    const CalendarPopover = React.forwardRef<  React.ElementRef<typeof PopoverPrimitives.Content>,  React.ComponentProps<typeof PopoverPrimitives.Content>>(({ align, className, children, ...props }, forwardedRef) => {  return (    <PopoverPrimitives.Portal>      <PopoverPrimitives.Content        ref={forwardedRef}        sideOffset={10}        side="bottom"        align={align}        avoidCollisions        onOpenAutoFocus={(e) => e.preventDefault()}        className={cx(          // base          "relative z-50 w-fit rounded-md border text-sm shadow-xl shadow-black/[2.5%]",          // widths          "min-w-[calc(var(--radix-select-trigger-width)-2px)] max-w-[95vw]",          // border color          "border-gray-300 dark:border-gray-800",          // background color          "bg-white dark:bg-gray-950",          // transition          "will-change-[transform,opacity]",          "data-[state=closed]:animate-hide",          "data-[state=open]:data-[side=bottom]:animate-slideDownAndFade data-[state=open]:data-[side=left]:animate-slideLeftAndFade data-[state=open]:data-[side=right]:animate-slideRightAndFade data-[state=open]:data-[side=top]:animate-slideUpAndFade",          className,        )}        {...props}      >        {children}      </PopoverPrimitives.Content>    </PopoverPrimitives.Portal>  )})
    CalendarPopover.displayName = "DatePicker.CalendarPopover"
    //#region Preset// ============================================================================
    export type DateRange = {  from: Date | undefined  to?: Date | undefined}
    interface Preset {  label: string}
    interface DatePreset extends Preset {  date: Date}
    interface DateRangePreset extends Preset {  dateRange: DateRange}
    type PresetContainerProps<TPreset extends Preset, TValue> = {  presets: TPreset[]  onSelect: (value: TValue) => void  currentValue?: TValue}
    const PresetContainer = <TPreset extends Preset, TValue>({  // Available preset configurations  presets,  // Event handler when a preset is selected  onSelect,  // Currently selected preset  currentValue,}: PresetContainerProps<TPreset, TValue>) => {  const isDateRangePresets = (preset: any): preset is DateRangePreset => {    return "dateRange" in preset  }
      const isDatePresets = (preset: any): preset is DatePreset => {    return "date" in preset  }
      const handleClick = (preset: TPreset) => {    if (isDateRangePresets(preset)) {      onSelect(preset.dateRange as TValue)    } else if (isDatePresets(preset)) {      onSelect(preset.date as TValue)    }  }
      const compareDates = (date1: Date, date2: Date) => {    return (      date1.getDate() === date2.getDate() &&      date1.getMonth() === date2.getMonth() &&      date1.getFullYear() === date2.getFullYear()    )  }
      const compareRanges = (range1: DateRange, range2: DateRange) => {    const from1 = range1.from    const from2 = range2.from
        let equalFrom = false
        if (from1 && from2) {      const sameFrom = compareDates(from1, from2)
          if (sameFrom) {        equalFrom = true      }    }
        const to1 = range1.to    const to2 = range2.to
        let equalTo = false
        if (to1 && to2) {      const sameTo = compareDates(to1, to2)
          if (sameTo) {        equalTo = true      }    }
        return equalFrom && equalTo  }
      const matchesCurrent = (preset: TPreset) => {    if (isDateRangePresets(preset)) {      const value = currentValue as DateRange | undefined
          return value && compareRanges(value, preset.dateRange)    } else if (isDatePresets(preset)) {      const value = currentValue as Date | undefined
          return value && compareDates(value, preset.date)    }
        return false  }
      return (    <ul role="list" className="flex items-start gap-x-2 sm:flex-col">      {presets.map((preset, index) => {        return (          <li key={index} className="sm:w-full sm:py-px">            <button              title={preset.label}              className={cx(                // base                "relative w-full overflow-hidden text-ellipsis whitespace-nowrap rounded border px-2.5 py-1.5 text-left text-base shadow-sm outline-none transition-all sm:border-none sm:py-2 sm:text-sm sm:shadow-none",                // text color                "text-gray-700 dark:text-gray-300",                // border color                "border-gray-300 dark:border-gray-800",                // focus                focusRing,                // background color                "focus-visible:bg-gray-100 focus-visible:dark:bg-gray-900",                "hover:bg-gray-100 hover:dark:bg-gray-900",                {                  "bg-gray-100 dark:bg-gray-900": matchesCurrent(preset),                },              )}              onClick={() => handleClick(preset)}              aria-label={`Select ${preset.label}`}            >              <span>{preset.label}</span>            </button>          </li>        )      })}    </ul>  )}
    PresetContainer.displayName = "DatePicker.PresetContainer"
    //#region Date Picker Shared// ============================================================================
    const formatDate = (  date: Date,  locale: Locale,  includeTime?: boolean,): string => {  const usesAmPm = !isBrowserLocaleClockType24h()  let dateString: string
      if (includeTime) {    dateString = usesAmPm      ? format(date, "dd MMM, yyyy h:mm a", { locale })      : format(date, "dd MMM, yyyy HH:mm", { locale })  } else {    dateString = format(date, "dd MMM, yyyy", { locale })  }
      return dateString}
    type CalendarProps = {  fromYear?: number  toYear?: number  fromMonth?: Date  toMonth?: Date  fromDay?: Date  toDay?: Date  fromDate?: Date  toDate?: Date  locale?: Locale}
    type Translations = {  cancel?: string  apply?: string  start?: string  end?: string  range?: string}
    interface PickerProps extends CalendarProps {  className?: string  disabled?: boolean  disabledDays?: Matcher | Matcher[] | undefined  required?: boolean  showTimePicker?: boolean  placeholder?: string  enableYearNavigation?: boolean  disableNavigation?: boolean  hasError?: boolean  id?: string  // Customize the date picker for different languages.  translations?: Translations  align?: "center" | "end" | "start"  "aria-invalid"?: boolean  "aria-label"?: string  "aria-labelledby"?: string  "aria-required"?: boolean}
    //#region Single Date Picker// ============================================================================
    interface SingleProps extends Omit<PickerProps, "translations"> {  presets?: DatePreset[]  defaultValue?: Date  value?: Date  onChange?: (date: Date | undefined) => void  translations?: Omit<Translations, "range">}
    const SingleDatePicker = ({  defaultValue,  value,  onChange,  presets,  disabled,  disabledDays,  disableNavigation,  className,  showTimePicker,  placeholder = "Select date",  hasError,  translations,  enableYearNavigation = false,  locale = enUS,  align = "center",  ...props}: SingleProps) => {  const [open, setOpen] = React.useState(false)  const [date, setDate] = React.useState<Date | undefined>(    value ?? defaultValue ?? undefined,  )  const [month, setMonth] = React.useState<Date | undefined>(date)
      const [time, setTime] = React.useState<TimeValue>(    value      ? new Time(value.getHours(), value.getMinutes())      : defaultValue        ? new Time(defaultValue.getHours(), defaultValue.getMinutes())        : new Time(0, 0),  )
      const initialDate = React.useMemo(() => {    return date    // eslint-disable-next-line react-hooks/exhaustive-deps  }, [open])
      React.useEffect(() => {    setDate(value ?? defaultValue ?? undefined)  }, [value, defaultValue])
      React.useEffect(() => {    if (date) {      setMonth(date)    }  }, [date])
      React.useEffect(() => {    if (!open) {      setMonth(date)    }    // eslint-disable-next-line react-hooks/exhaustive-deps  }, [open])
      const onCancel = () => {    setDate(initialDate)    setTime(      initialDate        ? new Time(initialDate.getHours(), initialDate.getMinutes())        : new Time(0, 0),    )    setOpen(false)  }
      const onOpenChange = (open: boolean) => {    if (!open) {      onCancel()    }
        setOpen(open)  }
      const onDateChange = (date: Date | undefined) => {    const newDate = date    if (showTimePicker) {      if (newDate && !time) {        setTime(new Time(0, 0))      }      if (newDate && time) {        newDate.setHours(time.hour)        newDate.setMinutes(time.minute)      }    }    setDate(newDate)  }
      const onTimeChange = (time: TimeValue) => {    setTime(time)
        if (!date) {      return    }
        const newDate = new Date(date.getTime())
        if (!time) {      newDate.setHours(0)      newDate.setMinutes(0)    } else {      newDate.setHours(time.hour)      newDate.setMinutes(time.minute)    }
        setDate(newDate)  }
      const formattedDate = React.useMemo(() => {    if (!date) {      return null    }
        return formatDate(date, locale, showTimePicker)  }, [date, locale, showTimePicker])
      const onApply = () => {    setOpen(false)    onChange?.(date)  }
      React.useEffect(() => {    setDate(value ?? defaultValue ?? undefined)    setTime(      value        ? new Time(value.getHours(), value.getMinutes())        : defaultValue          ? new Time(defaultValue.getHours(), defaultValue.getMinutes())          : new Time(0, 0),    )  }, [value, defaultValue])
      return (    <PopoverPrimitives.Root open={open} onOpenChange={onOpenChange}>      <Trigger        placeholder={placeholder}        disabled={disabled}        className={className}        hasError={hasError}        aria-required={props.required || props["aria-required"]}        aria-invalid={props["aria-invalid"]}        aria-label={props["aria-label"]}        aria-labelledby={props["aria-labelledby"]}      >        {formattedDate}      </Trigger>      <CalendarPopover align={align}>        <div className="flex">          <div className="flex flex-col sm:flex-row sm:items-start">            {presets && presets.length > 0 && (              <div                className={cx(                  "relative flex h-14 w-full items-center sm:h-full sm:w-40",                  "border-b border-gray-300 sm:border-b-0 sm:border-r dark:border-gray-800",                  "overflow-auto",                )}              >                <div className="absolute px-2 pr-2 sm:inset-0 sm:left-0 sm:py-2">                  <PresetContainer                    currentValue={date}                    presets={presets}                    onSelect={onDateChange}                  />                </div>              </div>            )}            <div>              <CalendarPrimitive                mode="single"                month={month}                onMonthChange={setMonth}                selected={date}                onSelect={onDateChange}                disabled={disabledDays}                locale={locale}                enableYearNavigation={enableYearNavigation}                disableNavigation={disableNavigation}                initialFocus                {...props}              />              {showTimePicker && (                <div className="border-t border-gray-300 p-3 dark:border-gray-800">                  <TimeInput                    aria-label="Time"                    onChange={onTimeChange}                    isDisabled={!date}                    value={time}                    isRequired={props.required}                  />                </div>              )}              <div className="flex items-center gap-x-2 border-t border-gray-300 p-3 dark:border-gray-800">                <Button                  variant="secondary"                  className="h-8 w-full"                  type="button"                  onClick={onCancel}                >                  {translations?.cancel ?? "Cancel"}                </Button>                <Button                  variant="primary"                  className="h-8 w-full"                  type="button"                  onClick={onApply}                >                  {translations?.apply ?? "Apply"}                </Button>              </div>            </div>          </div>        </div>      </CalendarPopover>    </PopoverPrimitives.Root>  )}
    //#region Range Date Picker// ============================================================================
    interface RangeProps extends PickerProps {  presets?: DateRangePreset[]  defaultValue?: DateRange  value?: DateRange  onChange?: (dateRange: DateRange | undefined) => void}
    const RangeDatePicker = ({  defaultValue,  value,  onChange,  presets,  disabled,  disableNavigation,  disabledDays,  enableYearNavigation = false,  locale = enUS,  showTimePicker,  placeholder = "Select date range",  hasError,  translations,  align = "center",  className,  ...props}: RangeProps) => {  const [open, setOpen] = React.useState(false)  const [range, setRange] = React.useState<DateRange | undefined>(    value ?? defaultValue ?? undefined,  )  const [month, setMonth] = React.useState<Date | undefined>(range?.from)
      const [startTime, setStartTime] = React.useState<TimeValue>(    value?.from      ? new Time(value.from.getHours(), value.from.getMinutes())      : defaultValue?.from        ? new Time(defaultValue.from.getHours(), defaultValue.from.getMinutes())        : new Time(0, 0),  )  const [endTime, setEndTime] = React.useState<TimeValue>(    value?.to      ? new Time(value.to.getHours(), value.to.getMinutes())      : defaultValue?.to        ? new Time(defaultValue.to.getHours(), defaultValue.to.getMinutes())        : new Time(0, 0),  )
      const initialRange = React.useMemo(() => {    return range    // eslint-disable-next-line react-hooks/exhaustive-deps  }, [open])
      React.useEffect(() => {    setRange(value ?? defaultValue ?? undefined)  }, [value, defaultValue])
      React.useEffect(() => {    if (range) {      setMonth(range.from)    }  }, [range])
      React.useEffect(() => {    if (!open) {      setMonth(range?.from)    }    // eslint-disable-next-line react-hooks/exhaustive-deps  }, [open])
      const onRangeChange = (range: DateRange | undefined) => {    const newRange = range    if (showTimePicker) {      if (newRange?.from && !startTime) {        setStartTime(new Time(0, 0))      }
          if (newRange?.to && !endTime) {        setEndTime(new Time(0, 0))      }
          if (newRange?.from && startTime) {        newRange.from.setHours(startTime.hour)        newRange.from.setMinutes(startTime.minute)      }
          if (newRange?.to && endTime) {        newRange.to.setHours(endTime.hour)        newRange.to.setMinutes(endTime.minute)      }    }
        setRange(newRange)  }
      const onCancel = () => {    setRange(initialRange)    setStartTime(      initialRange?.from        ? new Time(initialRange.from.getHours(), initialRange.from.getMinutes())        : new Time(0, 0),    )    setEndTime(      initialRange?.to        ? new Time(initialRange.to.getHours(), initialRange.to.getMinutes())        : new Time(0, 0),    )    setOpen(false)  }
      const onOpenChange = (open: boolean) => {    if (!open) {      onCancel()    }
        setOpen(open)  }
      const onTimeChange = (time: TimeValue, pos: "start" | "end") => {    switch (pos) {      case "start":        setStartTime(time)        break      case "end":        setEndTime(time)        break    }
        if (!range) {      return    }
        if (pos === "start") {      if (!range.from) {        return      }
          const newDate = new Date(range.from.getTime())
          if (!time) {        newDate.setHours(0)        newDate.setMinutes(0)      } else {        newDate.setHours(time.hour)        newDate.setMinutes(time.minute)      }
          setRange({        ...range,        from: newDate,      })    }
        if (pos === "end") {      if (!range.to) {        return      }
          const newDate = new Date(range.to.getTime())
          if (!time) {        newDate.setHours(0)        newDate.setMinutes(0)      } else {        newDate.setHours(time.hour)        newDate.setMinutes(time.minute)      }
          setRange({        ...range,        to: newDate,      })    }  }
      React.useEffect(() => {    setRange(value ?? defaultValue ?? undefined)
        setStartTime(      value?.from        ? new Time(value.from.getHours(), value.from.getMinutes())        : defaultValue?.from          ? new Time(              defaultValue.from.getHours(),              defaultValue.from.getMinutes(),            )          : new Time(0, 0),    )    setEndTime(      value?.to        ? new Time(value.to.getHours(), value.to.getMinutes())        : defaultValue?.to          ? new Time(defaultValue.to.getHours(), defaultValue.to.getMinutes())          : new Time(0, 0),    )  }, [value, defaultValue])
      const displayRange = React.useMemo(() => {    if (!range) {      return null    }
        return `${range.from ? formatDate(range.from, locale, showTimePicker) : ""} - ${      range.to ? formatDate(range.to, locale, showTimePicker) : ""    }`  }, [range, locale, showTimePicker])
      const onApply = () => {    setOpen(false)    onChange?.(range)  }
      return (    <PopoverPrimitives.Root open={open} onOpenChange={onOpenChange}>      <Trigger        placeholder={placeholder}        disabled={disabled}        className={className}        hasError={hasError}        aria-required={props.required || props["aria-required"]}        aria-invalid={props["aria-invalid"]}        aria-label={props["aria-label"]}        aria-labelledby={props["aria-labelledby"]}      >        {displayRange}      </Trigger>      <CalendarPopover align={align}>        <div className="flex">          <div className="flex flex-col overflow-x-scroll sm:flex-row sm:items-start">            {presets && presets.length > 0 && (              <div                className={cx(                  "relative flex h-16 w-full items-center sm:h-full sm:w-40",                  "border-b border-gray-300 sm:border-b-0 sm:border-r dark:border-gray-800",                  "overflow-auto",                )}              >                <div className="absolute px-3 sm:inset-0 sm:left-0 sm:p-2">                  <PresetContainer                    currentValue={range}                    presets={presets}                    onSelect={onRangeChange}                  />                </div>              </div>            )}            <div className="overflow-x-scroll">              <CalendarPrimitive                mode="range"                selected={range}                onSelect={onRangeChange}                month={month}                onMonthChange={setMonth}                numberOfMonths={2}                disabled={disabledDays}                disableNavigation={disableNavigation}                enableYearNavigation={enableYearNavigation}                locale={locale}                initialFocus                className="overflow-x-scroll"                classNames={{                  months:                    "flex flex-row divide-x divide-gray-300 dark:divide-gray-800 overflow-x-scroll",                }}                {...props}              />              {showTimePicker && (                <div className="flex items-center justify-evenly gap-x-3 border-t border-gray-300 p-3 dark:border-gray-800">                  <div className="flex flex-1 items-center gap-x-2">                    <span className="dark:text-gray-30 text-gray-700">                      {translations?.start ?? "Start"}:                    </span>                    <TimeInput                      value={startTime}                      onChange={(v) => onTimeChange(v, "start")}                      aria-label="Start date time"                      isDisabled={!range?.from}                      isRequired={props.required}                    />                  </div>                  <RiSubtractFill className="size-4 shrink-0 text-gray-400" />                  <div className="flex flex-1 items-center gap-x-2">                    <span className="dark:text-gray-30 text-gray-700">                      {translations?.end ?? "End"}:                    </span>                    <TimeInput                      value={endTime}                      onChange={(v) => onTimeChange(v, "end")}                      aria-label="End date time"                      isDisabled={!range?.to}                      isRequired={props.required}                    />                  </div>                </div>              )}              <div className="border-t border-gray-300 p-3 sm:flex sm:items-center sm:justify-between dark:border-gray-800">                <p className="tabular-nums text-gray-900 dark:text-gray-50">                  <span className="text-gray-700 dark:text-gray-300">                    {translations?.range ?? "Range"}:                  </span>{" "}                  <span className="font-medium">{displayRange}</span>                </p>                <div className="mt-2 flex items-center gap-x-2 sm:mt-0">                  <Button                    variant="secondary"                    className="h-8 w-full sm:w-fit"                    type="button"                    onClick={onCancel}                  >                    {translations?.cancel ?? "Cancel"}                  </Button>                  <Button                    variant="primary"                    className="h-8 w-full sm:w-fit"                    type="button"                    onClick={onApply}                  >                    {translations?.apply ?? "Apply"}                  </Button>                </div>              </div>            </div>          </div>        </div>      </CalendarPopover>    </PopoverPrimitives.Root>  )}
    //#region Preset Validation// ============================================================================
    const validatePresets = (  presets: DateRangePreset[] | DatePreset[],  rules: PickerProps,) => {  const { toYear, fromYear, fromMonth, toMonth, fromDay, toDay } = rules
      if (presets && presets.length > 0) {    const fromYearToUse = fromYear    const toYearToUse = toYear
        presets.forEach((preset) => {      if ("date" in preset) {        const presetYear = preset.date.getFullYear()
            if (fromYear && presetYear < fromYear) {          throw new Error(            `Preset ${preset.label} is before fromYear           ${fromYearToUse}.`,          )        }
            if (toYear && presetYear > toYear) {          throw new Error(            `Preset ${preset.label} is after toYear           ${toYearToUse}.`,          )        }
            if (fromMonth) {          const presetMonth = preset.date.getMonth()
              if (presetMonth < fromMonth.getMonth()) {            throw new Error(              `Preset ${preset.label} is before fromMonth             ${fromMonth}.`,            )          }        }
            if (toMonth) {          const presetMonth = preset.date.getMonth()
              if (presetMonth > toMonth.getMonth()) {            throw new Error(              `Preset ${preset.label} is after toMonth             ${toMonth}.`,            )          }        }
            if (fromDay) {          const presetDay = preset.date.getDate()
              if (presetDay < fromDay.getDate()) {            throw new Error(              `Preset ${preset.label} is before fromDay             ${fromDay}.`,            )          }        }
            if (toDay) {          const presetDay = preset.date.getDate()
              if (presetDay > toDay.getDate()) {            throw new Error(              `Preset ${preset.label} is after toDay             ${format(                toDay,                "MMM dd, yyyy",              )}.`,            )          }        }      }
          if ("dateRange" in preset) {        const presetFromYear = preset.dateRange.from?.getFullYear()        const presetToYear = preset.dateRange.to?.getFullYear()
            if (presetFromYear && fromYear && presetFromYear < fromYear) {          throw new Error(            `Preset ${preset.label}'s 'from' is before fromYear           ${fromYearToUse}.`,          )        }
            if (presetToYear && toYear && presetToYear > toYear) {          throw new Error(            `Preset ${preset.label}'s 'to' is after toYear           ${toYearToUse}.`,          )        }
            if (fromMonth) {          const presetMonth = preset.dateRange.from?.getMonth()
              if (presetMonth && presetMonth < fromMonth.getMonth()) {            throw new Error(              `Preset ${preset.label}'s 'from' is before fromMonth             ${format(                fromMonth,                "MMM, yyyy",              )}.`,            )          }        }
            if (toMonth) {          const presetMonth = preset.dateRange.to?.getMonth()
              if (presetMonth && presetMonth > toMonth.getMonth()) {            throw new Error(              `Preset ${preset.label}'s 'to' is after toMonth             ${format(                toMonth,                "MMM, yyyy",              )}.`,            )          }        }
            if (fromDay) {          const presetDay = preset.dateRange.from?.getDate()
              if (presetDay && presetDay < fromDay.getDate()) {            throw new Error(              `Preset ${                preset.dateRange.from              }'s 'from' is before fromDay             ${format(fromDay, "MMM dd, yyyy")}.`,            )          }        }
            if (toDay) {          const presetDay = preset.dateRange.to?.getDate()
              if (presetDay && presetDay > toDay.getDate()) {            throw new Error(              `Preset ${preset.label}'s 'to' is after toDay             ${format(                toDay,                "MMM dd, yyyy",              )}.`,            )          }        }      }    })  }}
    //#region Types & Exports// ============================================================================
    type SingleDatePickerProps = {  presets?: DatePreset[]  defaultValue?: Date  value?: Date  onChange?: (date: Date | undefined) => void} & PickerProps
    const DatePicker = ({ presets, ...props }: SingleDatePickerProps) => {  if (presets) {    validatePresets(presets, props)  }
      return <SingleDatePicker presets={presets} {...(props as SingleProps)} />}
    DatePicker.displayName = "DatePicker"
    type RangeDatePickerProps = {  presets?: DateRangePreset[]  defaultValue?: DateRange  value?: DateRange  onChange?: (dateRange: DateRange | undefined) => void} & PickerProps
    const DateRangePicker = ({ presets, ...props }: RangeDatePickerProps) => {  if (presets) {    validatePresets(presets, props)  }
      return <RangeDatePicker presets={presets} {...(props as RangeProps)} />}
    DateRangePicker.displayName = "DateRangePicker"
    export { DatePicker, DateRangePicker, type DatePreset, type DateRangePreset }
    
  4. 4

    Update tailwind.config.ts

    import type { Config } from 'tailwindcss';export default {  // ...  theme: {    extend: {      keyframes: {        hide: {          from: { opacity: "1" },          to: { opacity: "0" },        },        slideDownAndFade: {          from: { opacity: "0", transform: "translateY(-6px)" },          to: { opacity: "1", transform: "translateY(0)" },        },        slideLeftAndFade: {          from: { opacity: "0", transform: "translateX(6px)" },          to: { opacity: "1", transform: "translateX(0)" },        },        slideUpAndFade: {          from: { opacity: "0", transform: "translateY(6px)" },          to: { opacity: "1", transform: "translateY(0)" },        },        slideRightAndFade: {          from: { opacity: "0", transform: "translateX(-6px)" },          to: { opacity: "1", transform: "translateX(0)" },        },      },      animation: {        hide: "hide 150ms cubic-bezier(0.16, 1, 0.3, 1)",        slideDownAndFade:          "slideDownAndFade 150ms cubic-bezier(0.16, 1, 0.3, 1)",        slideLeftAndFade:          "slideLeftAndFade 150ms cubic-bezier(0.16, 1, 0.3, 1)",        slideUpAndFade: "slideUpAndFade 150ms cubic-bezier(0.16, 1, 0.3, 1)",        slideRightAndFade:          "slideRightAndFade 150ms cubic-bezier(0.16, 1, 0.3, 1)",      },    },  },  // ...} satisfies Config;

Example: With year navigation

Selected Date: None

Example: With presets

Selected Date: None

Example: With French locale and many presets

Selected Date: None

Example: Controlled with time input

Select a date

Example: With disabled input and error

Example: With disabled dates after today

Selected Date: None

API Reference: Date Picker

This component uses the React Day Picker API.

defaultValue
Date
Date selected by default (uncontrolled).
onChange
(date: undefined | Date) => void
Event handler when the selection changes.
value
Date
The currently selected date.
presets
DatePreset[]
Array with preset options.
locale
Locale
The locale used for date formatting. Accepts a date-fns locale object.
enableYearNavigation
boolean
Add additional buttons to switch years.

Default: false

disabled
boolean
Disable the trigger.

Default: false

disabledDays
Matcher | Matcher[] | undefined
Disable specific dates or date ranges.
disableNavigation
boolean
Disable the year and month navigation.

Default: false

required
boolean
Mark the date picker input as required.
placeholder
string
Set a placeholder text.

Default: Set date

hasError
boolean
Style for erroneous input.

Default: false

id
string
Add an id.
translations
Translations = { cancel?: string apply?: string }
Use this to localize certain elements of the picker.

Default: cancel: "Cancel", apply: "Apply"

showTimePicker
boolean
Add a time picker.

Default: false

fromYear
number
Year to start showing the dates from.
toYear
number
Year to show dates to.
fromMonth
Date
Month to start showing the dates from.
toMonth
Date
Month to show dates to.
fromDay
Date
Day to start showing the dates from.
toDay
Date
Day to show dates to.
fromDate
Date
Date to start showing the dates from.
toDate
Date
Date to show dates to.