Inputs
Date Range Picker
A component for selecting a date rage.
Selected Range: None
"use client"
import React from "react"
import { DateRange, DateRangePicker } from "@/components/DatePicker"
export const DateRangePickerHero = () => { const [dateRange, setDateRange] = React.useState<DateRange | undefined>( undefined, ) return ( <div className="flex flex-col items-center gap-y-4"> <DateRangePicker value={dateRange} onChange={setDateRange} className="w-60" />
<p className="flex items-center rounded-md bg-gray-100 p-2 text-sm text-gray-500 dark:bg-gray-800 dark:text-gray-300"> Selected Range:{" "} {dateRange ? `${dateRange.from?.toLocaleDateString()} – ${dateRange.to?.toLocaleDateString() ?? ""}` : "None"} </p> </div> )}
Installation
npm install react-day-picker@8.10.1 date-fns @remixicon/react @radix-ui/react-popover tailwind-variants @internationalized/date @react-stately/datepicker @react-aria/datepicker
- 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 Date Picker [v1.0.4] "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 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-200 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// ============================================================================ 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>) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const isDateRangePresets = (preset: any): preset is DateRangePreset => { return "dateRange" in preset } // eslint-disable-next-line @typescript-eslint/no-explicit-any 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 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-200 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 tremor-id="tremor-raw" 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-200 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-200 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-200 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 tremor-id="tremor-raw" 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-auto 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-200 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-auto"> <CalendarPrimitive mode="range" selected={range} onSelect={onRangeChange} month={month} onMonthChange={setMonth} numberOfMonths={2} disabled={disabledDays} disableNavigation={disableNavigation} enableYearNavigation={enableYearNavigation} locale={locale} initialFocus classNames={{ months: "flex flex-row divide-x divide-gray-200 dark:divide-gray-800 overflow-x-auto", }} {...props} /> {showTimePicker && ( <div className="flex items-center justify-evenly gap-x-3 border-t border-gray-200 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-200 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, type DateRange,}
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 Range: None
"use client"
import React from "react"import { DateRange, DateRangePicker } from "@/components/DatePicker"
export const DateRangePickerYearNavigationExample = () => { const [dateRange, setDateRange] = React.useState<DateRange | undefined>( undefined, ) return ( <div className="flex flex-col items-center gap-y-4"> <DateRangePicker enableYearNavigation value={dateRange} onChange={setDateRange} className="w-60" /> <p className="flex items-center rounded-md bg-gray-100 p-2 text-sm text-gray-500 dark:bg-gray-800 dark:text-gray-300"> Selected Range:{" "} {dateRange ? `${dateRange.from?.toLocaleDateString()} – ${dateRange.to?.toLocaleDateString() ?? ""}` : "None"} </p> </div> )}
Example: With presets
Selected Range: None
"use client"
import React from "react"import { DateRange, DateRangePicker } from "@/components/DatePicker"
export const DateRangePickerPresetsExample = () => { const [dateRange, setDateRange] = React.useState<DateRange | undefined>( undefined, ) const presets = [ { label: "Today", dateRange: { from: new Date(), to: new Date(), }, }, { label: "Last 7 days", dateRange: { from: new Date(new Date().setDate(new Date().getDate() - 7)), to: new Date(), }, }, { label: "Last 30 days", dateRange: { from: new Date(new Date().setDate(new Date().getDate() - 30)), to: new Date(), }, }, { label: "Last 3 months", dateRange: { from: new Date(new Date().setMonth(new Date().getMonth() - 3)), to: new Date(), }, }, { label: "Last 6 months", dateRange: { from: new Date(new Date().setMonth(new Date().getMonth() - 6)), to: new Date(), }, }, { label: "Month to date", dateRange: { from: new Date(new Date().setDate(1)), to: new Date(), }, }, { label: "Year to date", dateRange: { from: new Date(new Date().setFullYear(new Date().getFullYear(), 0, 1)), to: new Date(), }, }, ] return ( <div className="flex flex-col items-center gap-y-4"> <DateRangePicker presets={presets} value={dateRange} onChange={setDateRange} className="w-60" />
<p className="flex items-center rounded-md bg-gray-100 p-2 text-sm text-gray-500 dark:bg-gray-800 dark:text-gray-300"> Selected Range:{" "} {dateRange ? `${dateRange.from?.toLocaleDateString()} – ${dateRange.to?.toLocaleDateString() ?? ""}` : "None"} </p> </div> )}
Example: With French locale and many presets
Selected Range: None
"use client"
import React from "react"import { fr } from "date-fns/locale"import { DateRange, DateRangePicker } from "@/components/DatePicker"
export const DateRangePickerLocaleExample = () => { const [dateRange, setDateRange] = React.useState<DateRange | undefined>( undefined, ) const presets = [ { label: "Aujourd'hui", dateRange: { from: new Date(), to: new Date(), }, }, { label: "7 derniers jours", dateRange: { from: new Date(new Date().setDate(new Date().getDate() - 7)), to: new Date(), }, }, { label: "30 derniers jours", dateRange: { from: new Date(new Date().setDate(new Date().getDate() - 30)), to: new Date(), }, }, { label: "3 derniers mois", dateRange: { from: new Date(new Date().setMonth(new Date().getMonth() - 3)), to: new Date(), }, }, { label: "6 derniers mois", dateRange: { from: new Date(new Date().setMonth(new Date().getMonth() - 6)), to: new Date(), }, }, { label: "Mois en cours", dateRange: { from: new Date(new Date().setDate(1)), to: new Date(), }, }, { label: "Année en cours", dateRange: { from: new Date(new Date().setFullYear(new Date().getFullYear(), 0, 1)), to: new Date(), }, }, { label: "Mois précédent", dateRange: { from: new Date( new Date().setFullYear( new Date().getFullYear(), new Date().getMonth() - 1, 1, ), ), to: new Date( new Date().setFullYear( new Date().getFullYear(), new Date().getMonth(), 0, ), ), }, }, { label: "Semaine précédente", dateRange: { from: new Date( new Date().setDate(new Date().getDate() - new Date().getDay() - 6), ), to: new Date( new Date().setDate(new Date().getDate() - new Date().getDay()), ), }, }, { label: "30 prochains jours", dateRange: { from: new Date(), to: new Date(new Date().setDate(new Date().getDate() + 29)), }, }, { label: "Cette semaine", dateRange: { from: new Date( new Date().setDate(new Date().getDate() - new Date().getDay()), ), to: new Date( new Date().setDate(new Date().getDate() - new Date().getDay() + 6), ), }, }, { label: "7 Prochains jours", dateRange: { from: new Date(), to: new Date(new Date().setDate(new Date().getDate() + 6)), }, }, ] return ( <div className="flex flex-col items-center gap-y-4"> <DateRangePicker placeholder="Choisissez une date" translations={{ cancel: "Annuler", apply: "Appliquer", range: "Intervalle de dates", }} locale={fr} presets={presets} value={dateRange} onChange={setDateRange} className="w-60" />
<p className="flex items-center rounded-md bg-gray-100 p-2 text-sm text-gray-500 dark:bg-gray-800 dark:text-gray-300"> Selected Range:{" "} {dateRange ? `${dateRange.from?.toLocaleDateString()} – ${dateRange.to?.toLocaleDateString() ?? ""}` : "None"} </p> </div> )}
Example: Controlled with time input
Select a date range
"use client"
import React from "react"
import { Button } from "@/components/Button"import { DateRange, DateRangePicker } from "@/components/DatePicker"
export const DateRangePickerTimeExample = () => { const [value, setValue] = React.useState<DateRange | undefined>(undefined)
React.useEffect(() => { console.log("Value changed: ", value) }, [value])
return ( <> <div className="mx-auto flex max-w-sm flex-col gap-2"> <p className="text-gray-500"> {value ? `${value.from?.toString()} – ${value.to?.toString()}` : "Select a date range"} </p> <DateRangePicker showTimePicker value={value} onChange={(value) => { setValue(value) }} /> <div className="flex justify-end gap-2"> <Button variant="destructive" onClick={() => setValue(undefined)}> Reset </Button> <Button variant="secondary" onClick={() => setValue({ from: new Date(new Date().setDate(new Date().getDate() - 7)), to: new Date(), }) } > Week </Button> </div> </div> </> )}
Example: With disabled input and error
"use client"
import React from "react"import { DateRangePicker } from "@/components/DatePicker"
export const DateRangePickerDisabledErrorExample = () => { return ( <div className="flex flex-col items-center gap-y-4"> <DateRangePicker disabled className="w-60" /> <DateRangePicker hasError className="w-60" /> </div> )}
Example: With disabled dates after today
Selected Range: None
"use client"
import React from "react"import { DateRange, DateRangePicker } from "@/components/DatePicker"
export const DateRangePickerDisabledAfterTodayExample = () => { const [dateRange, setDateRange] = React.useState<DateRange | undefined>( undefined, ) return ( <div className="flex flex-col items-center gap-y-4"> <DateRangePicker toDate={new Date()} value={dateRange} onChange={setDateRange} className="w-60" />
<p className="flex items-center rounded-md bg-gray-100 p-2 text-sm text-gray-500 dark:bg-gray-800 dark:text-gray-300"> Selected Range:{" "} {dateRange ? `${dateRange.from?.toLocaleDateString()} – ${dateRange.to?.toLocaleDateString() ?? ""}` : "None"} </p> </div> )}
API Reference: Date Range Picker
This component uses the React Day Picker API.
- defaultValue
- Date selected by default (uncontrolled).
DateRange
- onChange
- Event handler when the selection changes.
(date: undefined | DateRange) => void
- value
- The currently selected date.
DateRange
- presets
- Array with preset options.
DateRangePreset[]
- locale
- The locale used for date formatting. Accepts a date-fns locale object.
Locale
- enableYearNavigation
- Add additional buttons to switch years.
boolean
Default: false
- disabled
- Disable the trigger.
boolean
Default: false
- disabledDays
- Disable specific dates or date ranges.
Matcher | Matcher[] | undefined
- disableNavigation
- Disable the year and month navigation.
boolean
Default: false
- required
- Mark the date picker input as required.
boolean
- placeholder
- Set a placeholder text.
string
Default: Set date
- hasError
- Style for erroneous input.
boolean
Default: false
- id
- Add an id.
string
- translations
- Use this to localize certain elements of the picker.
Translations = {
cancel?: string
apply?: string
range?: string
}
Default: cancel: "Cancel", apply: "Apply", range: "Range"
- showTimePicker
- Add a time picker.
boolean
Default: false
- fromYear
- Year to start showing the dates from.
number
- toYear
- Year to show dates to.
number
- fromMonth
- Month to start showing the dates from.
Date
- toMonth
- Month to show dates to.
Date
- fromDay
- Day to start showing the dates from.
Date
- toDay
- Day to show dates to.
Date
- fromDate
- Date to start showing the dates from.
Date
- toDate
- Date to show dates to.
Date