Inputs
Date Picker
A component for selecting a date.
Selected Date: None
"use client"
import React from "react"
import { DatePicker } from "@/components/DatePicker"
export const DatePickerHero = () => { const [date, setDate] = React.useState<Date | undefined>(undefined) return ( <div className="flex flex-col items-center gap-y-4"> <DatePicker value={date} onChange={setDate} 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 Date: {date ? date.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 Date: None
"use client"
import React from "react"
import { DatePicker } from "@/components/DatePicker"
export const DatePickerYearNavigationExample = () => { const [date, setDate] = React.useState<Date | undefined>(undefined) return ( <div className="flex flex-col items-center gap-y-4"> <DatePicker enableYearNavigation value={date} onChange={setDate} 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 Date: {date ? date.toLocaleDateString() : "None"} </p> </div> )}
Example: With presets
Selected Date: None
"use client"
import React from "react"import { DatePicker } from "@/components/DatePicker"
export const DatePickerPresetsExample = () => { const [date, setDate] = React.useState<Date | undefined>(undefined) const presets = [ { label: "Today", date: new Date(), }, { label: "Tomorrow", date: new Date(new Date().setDate(new Date().getDate() + 1)), }, { label: "A week from now", date: new Date(new Date().setDate(new Date().getDate() + 7)), }, { label: "A month from now", date: new Date(new Date().setMonth(new Date().getMonth() + 1)), }, { label: "6 months from now", date: new Date(new Date().setMonth(new Date().getMonth() + 6)), }, { label: "A year from now", date: new Date(new Date().setFullYear(new Date().getFullYear() + 1)), }, ] return ( <div className="flex flex-col items-center gap-y-4"> <DatePicker presets={presets} value={date} onChange={setDate} 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 Date: {date ? date.toLocaleDateString() : "None"} </p> </div> )}
Example: With French locale and many presets
Selected Date: None
"use client"
import React from "react"import { fr } from "date-fns/locale"import { DatePicker } from "@/components/DatePicker"
export const DatePickerLocaleExample = () => { const [date, setDate] = React.useState<Date | undefined>(undefined) const presets = [ { label: "Aujourd'hui", date: new Date(), }, { label: "Demain", date: new Date(new Date().setDate(new Date().getDate() + 1)), }, { label: "Dans une semaine", date: new Date(new Date().setDate(new Date().getDate() + 7)), }, { label: "Dans un mois", date: new Date(new Date().setMonth(new Date().getMonth() + 1)), }, { label: "Dans un an", date: new Date(new Date().setFullYear(new Date().getFullYear() + 1)), }, { label: "Ce weekend (samedi)", date: new Date( new Date().setDate(new Date().getDate() + (6 - new Date().getDay())), ), }, { label: "Premier jour du mois prochain", date: new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1), }, { label: "Dernier jour du mois", date: new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0), }, { label: "Dans six mois", date: new Date(new Date().setMonth(new Date().getMonth() + 6)), }, { label: "Dans trois mois", date: new Date(new Date().setMonth(new Date().getMonth() + 3)), }, { label: "Hier", date: new Date(new Date().setDate(new Date().getDate() - 1)), }, { label: "La semaine dernière", date: new Date(new Date().setDate(new Date().getDate() - 7)), }, { label: "Le mois dernier", date: new Date(new Date().setMonth(new Date().getMonth() - 1)), }, { label: "L'année dernière", date: new Date(new Date().setFullYear(new Date().getFullYear() - 1)), }, ] return ( <div className="flex flex-col items-center gap-y-4"> <DatePicker placeholder="Choisissez une date" translations={{ cancel: "Annuler", apply: "Appliquer" }} locale={fr} presets={presets} value={date} onChange={setDate} 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 Date: {date ? date.toLocaleDateString() : "None"} </p> </div> )}
Example: Controlled with time input
Select a date
"use client"
import React from "react"
import { Button } from "@/components/Button"import { DatePicker } from "@/components/DatePicker"
export const DatePickerTimeExample = () => { const [value, setValue] = React.useState<Date | undefined>(undefined)
return ( <> <div className="mx-auto flex max-w-xs flex-col gap-2"> <p className="text-gray-500"> {value ? value.toString() : "Select a date"} </p> <DatePicker 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(new Date())}> Now </Button> </div> </div> </> )}
Example: With disabled input and error
"use client"
import React from "react"import { DatePicker } from "@/components/DatePicker"
export const DatePickerDisabledErrorExample = () => { return ( <div className="flex flex-col items-center gap-y-4"> <DatePicker disabled className="w-60" /> <DatePicker hasError className="w-60" /> </div> )}
Example: With disabled dates after today
Selected Date: None
"use client"
import React from "react"import { DatePicker } from "@/components/DatePicker"
export const DatePickerDisabledAfterTodayExample = () => { const [date, setDate] = React.useState<Date | undefined>(undefined) return ( <div className="flex flex-col items-center gap-y-4"> <DatePicker toDate={new Date()} value={date} onChange={setDate} 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 Date: {date ? date.toLocaleDateString() : "None"} </p> </div> )}
Example: Custom Birth Date Picker
Date of Birth: None
"use client"
import React from "react"import * as PopoverPrimitives from "@radix-ui/react-popover"import { RiArrowLeftSLine, RiArrowRightSLine, RiCake2Fill, RiCalendar2Fill,} from "@remixicon/react"import { format, getYear, isSameMonth, setMonth, setYear, type Locale,} from "date-fns"import { enUS } from "date-fns/locale"import { DayPicker, Matcher, useDayPicker, useDayRender, useNavigation, type DayPickerSingleProps, type DayProps,} from "react-day-picker"import { tv, VariantProps } from "tailwind-variants"
import { Button } from "@/components/Button"import { SelectNative } from "@/components/SelectNative"import { cx, focusInput, focusRing, hasErrorInput } from "@/lib/utils"
//#region Tremor Custom Birth Date Calendarinterface 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={cx( "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 CalendarProps = | ({ mode: "single" } & SingleProps) | ({ mode?: undefined } & SingleProps)
const CalendarPrimitive = React.memo( ({ weekStartsOn = 1, numberOfMonths = 1, enableYearNavigation = false, disableNavigation, locale, className, classNames, ...props }: CalendarProps & { enableYearNavigation?: boolean }) => { return ( <DayPicker weekStartsOn={weekStartsOn} numberOfMonths={numberOfMonths} locale={locale} showOutsideDays={numberOfMonths === 1} className={cx(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: cx( "relative p-0 text-center focus-within:relative", "text-gray-900 dark:text-gray-50", ), day: cx( "size-9 rounded text-sm focus:z-10", "text-gray-900 dark:text-gray-50", "hover:bg-gray-200 hover:dark:bg-gray-700", focusRing, ), day_today: "font-semibold", day_selected: cx( "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: cx( "!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: ({ displayMonth }) => { const { goToMonth, nextMonth, previousMonth, displayMonths } = useNavigation() const { numberOfMonths } = useDayPicker()
const displayIndex = displayMonths.findIndex((month) => isSameMonth(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 currentSelectedYear = getYear(displayMonth) const currentYear = getYear(new Date()) const years = Array.from( { length: 101 }, (_, i) => currentYear - 100 + i, )
return ( <div className="flex items-center justify-between gap-4"> <div className="flex items-center gap-2"> <> <label htmlFor="year-select" className="sr-only" /> <SelectNative id="year-select" name="Select year" value={currentSelectedYear.toString()} onChange={(e) => goToMonth( setYear(displayMonth, parseInt(e.target.value)), ) } className="w-[100px] px-2 py-1" > {years.map((year) => ( <option key={year} value={year.toString()}> {year} </option> ))} </SelectNative> <label htmlFor="month-select" className="sr-only" /> <SelectNative id="month-select" name="Select month" value={displayMonth.getMonth().toString()} onChange={(e) => goToMonth( setMonth(displayMonth, parseInt(e.target.value)), ) } className="w-[120px] px-2 py-1" > {[ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ].map((month, index) => ( <option key={month} value={index.toString()}> {month} </option> ))} </SelectNative> </> </div> <div className="flex items-center gap-1"> {!hidePreviousButton && ( <NavigationButton disabled={disableNavigation || !previousMonth} aria-label="Go to previous month" onClick={() => previousMonth && goToMonth(previousMonth)} icon={RiArrowLeftSLine} /> )} {!hideNextButton && ( <NavigationButton disabled={disableNavigation || !nextMonth} aria-label="Go to next month" onClick={() => nextMonth && goToMonth(nextMonth)} icon={RiArrowRightSLine} /> )} </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={cx( "flex items-center justify-center", divProps.className, )} /> ) }
const { children: buttonChildren, className: buttonClassName, ...buttonPropsRest } = buttonProps
return ( <button ref={buttonRef} {...buttonPropsRest} type="button" className={cx("relative", buttonClassName)} > {buttonChildren} {today && ( <span className={cx( "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> ) }, }} tremor-id="tremor-raw" {...(props as SingleProps)} /> ) },)
CalendarPrimitive.displayName = "CalendarPrimitive"
//#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.memo( 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.memo( 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 Date Picker Shared// ============================================================================
const formatDate = (date: Date, locale: Locale): string => { let dateString: string
dateString = format(date, "dd MMM, yyyy", { locale })
return dateString}
type CalendarPickerProps = { 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 CalendarPickerProps { 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 SinglePickerProps extends Omit<PickerProps, "translations"> { defaultValue?: Date value?: Date onChange?: (date: Date | undefined) => void translations?: Omit<Translations, "range">}
const SingleDatePicker = ({ defaultValue, value, onChange, disabled, disabledDays, disableNavigation, className, showTimePicker, placeholder = "Select date", hasError, translations, locale = enUS, align = "center", ...props}: SinglePickerProps) => { const [open, setOpen] = React.useState(false) const [date, setDate] = React.useState<Date | undefined>( value ?? defaultValue ?? undefined, ) const [month, setMonth] = React.useState<Date | undefined>(date)
// eslint-disable-next-line react-hooks/exhaustive-deps const initialDate = React.useMemo(() => date, [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 = React.useCallback(() => { setDate(initialDate) setOpen(false) }, [initialDate])
const onOpenChange = React.useCallback( (open: boolean) => { if (!open) { onCancel() } setOpen(open) }, [onCancel], )
const onDateChange = React.useCallback((newDate: Date | undefined) => { setDate(newDate) }, [])
const formattedDate = React.useMemo(() => { if (!date) { return null } return formatDate(date, locale) }, [date, locale])
const onApply = React.useCallback(() => { setOpen(false) onChange?.(date) }, [onChange, date])
React.useEffect(() => { setDate(value ?? defaultValue ?? undefined) }, [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"> <div> <CalendarPrimitive mode="single" month={month} onMonthChange={setMonth} selected={date} onSelect={onDateChange} disabled={disabledDays} locale={locale} disableNavigation={disableNavigation} initialFocus {...props} /> <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" disabled={!date} onClick={onApply} > {translations?.apply ?? "Apply"} </Button> </div> </div> </div> </div> </CalendarPopover> </PopoverPrimitives.Root> )}
//#region Types & Exports// ============================================================================
type SingleDatePickerProps = { defaultValue?: Date value?: Date onChange?: (date: Date | undefined) => void} & PickerProps
const DatePicker = React.memo(({ ...props }: SingleDatePickerProps) => { return <SingleDatePicker {...(props as SinglePickerProps)} />})
DatePicker.displayName = "DatePicker"
export const DatePickerBirthDateExample = () => { const [date, setDate] = React.useState<Date | undefined>(undefined) return ( <div className="flex flex-col items-center gap-y-4"> <DatePicker toDate={new Date()} value={date} onChange={setDate} 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"> <RiCake2Fill className="ml-1 mr-2 size-4 text-blue-500" /> Date of Birth: {date ? date.toLocaleDateString() : "None"} </p> </div> )}
API Reference: Date Picker
This component uses the React Day Picker API.
- defaultValue
- Date selected by default (uncontrolled).
Date
- onChange
- Event handler when the selection changes.
(date: undefined | Date) => void
- value
- The currently selected date.
Date
- presets
- Array with preset options.
DatePreset[]
- 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
}
Default: cancel: "Cancel", apply: "Apply"
- 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