Date Picker
A popover-based date picker wrapping Calendar and Popover. Supports single, range, and multiple selection modes with smart auto-close behavior. Includes a DateTimePicker variant with a scrollable time slot panel.
Installation
npx shadcn@latest add @glrk-ui/date-pickerIf you haven't set up the prerequisites yet, check out Prerequest section.
Copy and paste the following code into your project.
import { format } from 'date-fns'
import { type DateRange } from '@daypicker/react'
export type MinuteStep = 1 | 5 | 10 | 15 | 20 | 30
export type HourFormat = 12 | 24
export type Selected = Date | Date[] | DateRange | undefined
export function formatSelected(selected: Selected, fmt: string): string | null {
if (!selected) return null
if (selected instanceof Date) return format(selected, fmt)
if (Array.isArray(selected)) {
if (!selected.length) return null
return selected.length === 1 ? format(selected[0]!, fmt) : `${selected.length} dates selected`
}
const range = selected as DateRange
if (!range.from) return null
if (!range.to) return format(range.from, fmt)
return `${format(range.from, fmt)} - ${format(range.to, fmt)}`
}
export function getDefaultMonth(selected: Selected): Date | undefined {
if (!selected) return undefined
if (selected instanceof Date) return selected
if (Array.isArray(selected)) return selected[0]
return (selected as DateRange).from
}
export function snapToStep(minutes: number, step: number): number {
const snapped = Math.round(minutes / step) * step
return snapped >= 60 ? 0 : snapped
}
export function snapTimeForward(
baseHour: number,
minMinute: number,
step: number,
): { hours: number; minutes: number } {
const snapped = Math.ceil(minMinute / step) * step
if (snapped >= 60) return { hours: baseHour + 1, minutes: 0 }
return { hours: baseHour, minutes: snapped }
}
export function defaultNow(step: number): Date {
const n = new Date()
n.setMinutes(snapToStep(n.getMinutes(), step))
n.setSeconds(0)
n.setMilliseconds(0)
return n
}
export function startOfDay(date: Date): number {
const d = new Date(date)
d.setHours(0, 0, 0, 0)
return d.getTime()
}
import * as React from 'react'
import { type DropdownProps } from '@daypicker/react'
import { cn } from '@/lib/utils'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
function CalendarDropdown({ options = [], value, onChange, disabled, className }: DropdownProps) {
const stringValue = value !== undefined ? String(value) : undefined
const labelMap = React.useMemo(
() => Object.fromEntries(options.map(o => [String(o.value), o.label])),
[options],
)
function handleValueChange(val: string | null) {
if (!onChange || val === null) return
const event = { target: { value: val } } as React.ChangeEvent<HTMLSelectElement>
onChange(event)
}
return (
<Select
value={stringValue}
onValueChange={handleValueChange as (val: string | null) => void}
disabled={disabled}
>
<SelectTrigger
className={cn(
'h-7 border-0 shadow-none bg-transparent hover:bg-accent px-2 pr-1 font-medium gap-0.5 text-sm',
className,
)}
>
<SelectValue>{() => labelMap[stringValue ?? ''] ?? stringValue}</SelectValue>
</SelectTrigger>
<SelectContent className="min-w-16">
{options.map(({ value: val, label, disabled: optDisabled }) => (
<SelectItem
key={val}
value={String(val)}
disabled={optDisabled}
className="px-2! [&_svg]:hidden"
>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
)
}
export { CalendarDropdown }
import * as React from 'react'
import { getDefaultClassNames, type DayButton, type Locale } from '@daypicker/react'
import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'
function CalendarDayButton({
className,
day,
modifiers,
locale,
...props
}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString(locale?.code)}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
'relative flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70',
defaultClassNames.day,
className,
)}
{...props}
/>
)
}
export { CalendarDayButton }
'use client'
import * as React from 'react'
import { DayPicker, getDefaultClassNames } from '@daypicker/react'
import { ChevronLeftIcon, ChevronRightIcon, ChevronDownIcon } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Button, buttonVariants } from '@/components/ui/button'
import { CalendarDayButton } from './calendar-day-button'
import { CalendarDropdown } from './calendar-dropdown'
function Calendar({
locale,
className,
classNames,
formatters,
components,
captionLayout = 'label',
buttonVariant = 'ghost',
showOutsideDays = true,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>['variant']
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
'group/calendar bg-background p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(7)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent',
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className,
)}
captionLayout={captionLayout}
locale={locale}
formatters={{
formatMonthDropdown: date => date.toLocaleString(locale?.code, { month: 'short' }),
...formatters,
}}
classNames={{
root: cn('w-fit', defaultClassNames.root),
months: cn('relative flex flex-col gap-4 md:flex-row', defaultClassNames.months),
month: cn('flex w-full flex-col gap-4', defaultClassNames.month),
nav: cn(
'pointer-events-none absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1',
defaultClassNames.nav,
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
'pointer-events-auto size-(--cell-size) p-0 select-none aria-disabled:opacity-50',
defaultClassNames.button_previous,
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
'pointer-events-auto size-(--cell-size) p-0 select-none aria-disabled:opacity-50',
defaultClassNames.button_next,
),
month_caption: cn(
'flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)',
defaultClassNames.month_caption,
),
dropdowns: cn(
'flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium',
defaultClassNames.dropdowns,
),
dropdown_root: cn('relative rounded-(--cell-radius)', defaultClassNames.dropdown_root),
dropdown: cn('absolute inset-0 bg-popover opacity-0', defaultClassNames.dropdown),
caption_label: cn(
'font-medium select-none',
captionLayout === 'label'
? 'text-sm'
: 'flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground',
defaultClassNames.caption_label,
),
month_grid: 'w-full border-collapse',
weekdays: cn('flex', defaultClassNames.weekdays),
weekday: cn(
'flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none',
defaultClassNames.weekday,
),
week: cn('mt-2 flex w-full', defaultClassNames.week),
week_number_header: cn('w-(--cell-size) select-none', defaultClassNames.week_number_header),
week_number: cn(
'text-[0.8rem] text-muted-foreground select-none',
defaultClassNames.week_number,
),
day: cn(
'group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)',
props.showWeekNumber
? '[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)'
: '[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)',
defaultClassNames.day,
),
range_start: cn(
'relative rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted',
defaultClassNames.range_start,
),
range_middle: cn('rounded-none', defaultClassNames.range_middle),
range_end: cn(
'relative rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted',
defaultClassNames.range_end,
),
today: cn(
'rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none',
defaultClassNames.today,
),
outside: cn(
'text-muted-foreground aria-selected:text-muted-foreground',
defaultClassNames.outside,
),
disabled: cn('text-muted-foreground opacity-50', defaultClassNames.disabled),
hidden: cn('invisible', defaultClassNames.hidden),
...classNames,
}}
components={{
Dropdown: CalendarDropdown,
Root: ({ className, rootRef, ...props }) => {
return <div data-slot="calendar" ref={rootRef} className={cn(className)} {...props} />
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === 'left') {
return <ChevronLeftIcon className={cn('size-4', className)} {...props} />
}
if (orientation === 'right') {
return <ChevronRightIcon className={cn('size-4', className)} {...props} />
}
return <ChevronDownIcon className={cn('size-4', className)} {...props} />
},
DayButton: props => <CalendarDayButton locale={locale} {...props} />,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
export { Calendar }
'use client'
import * as React from 'react'
import { type DateRange } from '@daypicker/react'
import { CalendarIcon } from 'lucide-react'
import { type Selected, formatSelected, getDefaultMonth } from './utils'
import { cn } from '@/lib/utils'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Button } from '@/components/ui/button'
import { Calendar } from './calendar'
type DatePickerProps = Omit<
React.ComponentProps<typeof Calendar>,
'mode' | 'selected' | 'onSelect'
> & {
open?: boolean
mode?: 'single' | 'multiple' | 'range'
align?: React.ComponentProps<typeof PopoverContent>['align']
selected?: Selected
dateFormat?: string
placeholder?: string
triggerProps?: Omit<React.ComponentProps<typeof Button>, 'children'>
onOpenChange?: (open: boolean) => void
onSelect?: (...args: any[]) => void
}
function DatePicker({
mode,
open,
selected,
triggerProps,
align = 'start',
dateFormat = 'dd/MM/yyyy',
placeholder = 'Pick a date',
onSelect,
onOpenChange,
...calendarProps
}: DatePickerProps) {
const [internalOpen, setInternalOpen] = React.useState(false)
const isControlled = open !== undefined
const open_ = isControlled ? open : internalOpen
function handleOpenChange(next: boolean) {
if (!isControlled) setInternalOpen(next)
onOpenChange?.(next)
}
function handleSelect(...args: any[]) {
onSelect?.(...args)
const effectiveMode = mode ?? 'single'
if (effectiveMode === 'single') {
handleOpenChange(false)
} else if (effectiveMode === 'range') {
const val = args[0] as DateRange | undefined
if (val?.from && val?.to) handleOpenChange(false)
}
}
const { className: triggerClassName, ...restTriggerProps } = triggerProps ?? {}
const label = formatSelected(selected, dateFormat)
return (
<Popover open={open_} onOpenChange={handleOpenChange}>
<PopoverTrigger
render={
<Button
variant="outline"
{...restTriggerProps}
className={cn(
'w-full pl-3 text-left font-normal shadow-xs',
!label && 'text-muted-foreground',
triggerClassName,
)}
>
{label ?? <span>{placeholder}</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" aria-hidden="true" />
</Button>
}
/>
<PopoverContent className="w-auto p-0" align={align}>
<Calendar
{...({
autoFocus: true,
captionLayout: 'dropdown',
defaultMonth: getDefaultMonth(selected),
mode: mode ?? 'single',
selected,
onSelect: handleSelect,
...calendarProps,
} as React.ComponentProps<typeof Calendar>)}
/>
</PopoverContent>
</Popover>
)
}
export { DatePicker }
export type { DatePickerProps }
'use client'
import * as React from 'react'
import { CalendarIcon, Clock } from 'lucide-react'
import { format } from 'date-fns'
import { cn } from '@/lib/utils'
import {
type MinuteStep,
type HourFormat,
snapToStep,
snapTimeForward,
defaultNow,
startOfDay,
} from './utils'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Button } from '@/components/ui/button'
import { Calendar } from './calendar'
type DateTimePickerProps = Omit<
React.ComponentProps<typeof Calendar>,
'mode' | 'selected' | 'onSelect' | 'disabled'
> & {
open?: boolean
align?: React.ComponentProps<typeof PopoverContent>['align']
minDate?: Date
maxDate?: Date
minTime?: Date
selected?: Date
placeholder?: string
dateFormat?: string
minuteStep?: MinuteStep
hourFormat?: HourFormat
disablePast?: boolean
triggerProps?: Omit<React.ComponentProps<typeof Button>, 'children'>
onSelect?: (date: Date | undefined) => void
onOpenChange?: (open: boolean) => void
}
function DateTimePicker({
open,
minDate,
maxDate,
minTime,
selected,
dateFormat,
triggerProps,
align = 'start',
minuteStep = 5,
hourFormat = 24,
placeholder = 'Pick date & time',
disablePast = false,
onSelect,
onOpenChange,
...calendarProps
}: DateTimePickerProps) {
const [internalOpen, setInternalOpen] = React.useState(false)
const isControlled = open !== undefined
const open_ = isControlled ? open : internalOpen
const [internalDate, setInternalDate] = React.useState<Date | undefined>(selected)
const slotRefs = React.useRef<Record<number, HTMLButtonElement | null>>({})
React.useEffect(() => {
setInternalDate(selected)
}, [selected])
function handleOpenChange(next: boolean) {
if (!isControlled) setInternalOpen(next)
onOpenChange?.(next)
}
const effectiveMinTime = React.useMemo(() => {
const candidates: Date[] = []
if (disablePast) candidates.push(new Date())
if (minTime) candidates.push(minTime)
if (!candidates.length) return null
return candidates.reduce((a, b) => (a > b ? a : b))
}, [disablePast, minTime])
const effectiveMinTotalMinutes = React.useMemo(
() =>
effectiveMinTime ? effectiveMinTime.getHours() * 60 + effectiveMinTime.getMinutes() : -1,
[effectiveMinTime],
)
const isMinTimeDay = React.useMemo(() => {
if (!effectiveMinTime) return false
const reference = internalDate ?? new Date()
return startOfDay(effectiveMinTime) === startOfDay(reference)
}, [effectiveMinTime, internalDate])
function commit(next: Date | undefined) {
setInternalDate(next)
onSelect?.(next)
}
function handleDaySelect(day: Date | undefined) {
if (!day) return
const base = internalDate ?? new Date()
const merged = new Date(day)
merged.setHours(base.getHours())
merged.setMinutes(snapToStep(base.getMinutes(), minuteStep))
merged.setSeconds(0)
merged.setMilliseconds(0)
if (effectiveMinTime) {
if (startOfDay(merged) === startOfDay(effectiveMinTime) && merged < effectiveMinTime) {
const { hours, minutes } = snapTimeForward(
effectiveMinTime.getHours(),
effectiveMinTime.getMinutes(),
minuteStep,
)
merged.setHours(hours)
merged.setMinutes(minutes)
merged.setSeconds(0)
}
}
commit(merged)
}
const timeSlots = React.useMemo(() => {
const total = Math.floor((24 * 60) / minuteStep)
return Array.from({ length: total }, (_, i) => {
const totalMinutes = i * minuteStep
return { hour: Math.floor(totalMinutes / 60), minute: totalMinutes % 60 }
})
}, [minuteStep])
function formatSlot(hour: number, minute: number): string {
const mm = String(minute).padStart(2, '0')
if (hourFormat === 24) return `${String(hour).padStart(2, '0')}:${mm}`
const h12 = hour % 12 === 0 ? 12 : hour % 12
return `${String(h12).padStart(2, '0')}:${mm} ${hour < 12 ? 'AM' : 'PM'}`
}
function isSlotDisabled(hour: number, minute: number): boolean {
if (effectiveMinTotalMinutes < 0 || !isMinTimeDay) return false
return hour * 60 + minute < effectiveMinTotalMinutes
}
const selectedSlotKey =
internalDate !== undefined ? internalDate.getHours() * 60 + internalDate.getMinutes() : -1
React.useEffect(() => {
if (selectedSlotKey >= 0) {
slotRefs.current[selectedSlotKey]?.scrollIntoView({ block: 'center' })
}
}, [selectedSlotKey, open])
function selectSlot(hour: number, minute: number) {
if (isSlotDisabled(hour, minute)) return
const base = internalDate ?? defaultNow(minuteStep)
const next = new Date(base)
next.setHours(hour)
next.setMinutes(minute)
next.setSeconds(0)
next.setMilliseconds(0)
commit(next)
}
function setNow() {
const n = new Date()
n.setMinutes(snapToStep(n.getMinutes(), minuteStep))
n.setSeconds(0)
n.setMilliseconds(0)
if (effectiveMinTime && n < effectiveMinTime) {
const { hours, minutes } = snapTimeForward(
effectiveMinTime.getHours(),
effectiveMinTime.getMinutes(),
minuteStep,
)
n.setFullYear(
effectiveMinTime.getFullYear(),
effectiveMinTime.getMonth(),
effectiveMinTime.getDate(),
)
n.setHours(hours)
n.setMinutes(minutes)
n.setSeconds(0)
}
commit(n)
}
const calendarDisabled = React.useMemo(() => {
const todayMs = disablePast ? startOfDay(new Date()) : -1
const minTimeMs = minTime ? startOfDay(minTime) : -1
const minDateMs = minDate ? startOfDay(minDate) : -1
const maxDateMs = maxDate ? new Date(maxDate).setHours(23, 59, 59, 999) : Infinity
return (date: Date) => {
const d = startOfDay(date)
if (todayMs >= 0 && d < todayMs) return true
if (minTimeMs >= 0 && d < minTimeMs) return true
if (minDateMs >= 0 && d < minDateMs) return true
if (date.getTime() > maxDateMs) return true
return false
}
}, [disablePast, minTime, minDate, maxDate])
const fmt = dateFormat ?? (hourFormat === 24 ? 'dd/MM/yyyy HH:mm' : 'dd/MM/yyyy hh:mm a')
const { className: triggerClassName, ...restTriggerProps } = triggerProps ?? {}
return (
<Popover open={open_} onOpenChange={handleOpenChange}>
<PopoverTrigger
render={
<Button
variant="outline"
{...restTriggerProps}
className={cn(
'w-full pl-3 text-left font-normal shadow-xs',
!internalDate && 'text-muted-foreground',
triggerClassName,
)}
>
{internalDate ? format(internalDate, fmt) : <span>{placeholder}</span>}
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" aria-hidden="true" />
</Button>
}
/>
<PopoverContent className="w-auto p-0 overflow-hidden" align={align} sideOffset={8}>
<div className="flex flex-col">
<div className="flex">
<div>
<Calendar
{...({
autoFocus: true,
mode: 'single',
selected: internalDate,
onSelect: handleDaySelect,
disabled: calendarDisabled,
...calendarProps,
} as React.ComponentProps<typeof Calendar>)}
/>
</div>
<div
className={cn(
'flex flex-col border-l overflow-hidden',
hourFormat === 12 ? 'w-24' : 'w-20',
)}
>
<div className="flex items-center justify-center gap-1.5 px-2 pt-3 pb-2 text-xs font-medium">
<Clock className="h-3.5 w-3.5 text-muted-foreground" aria-hidden="true" />
<span>Time</span>
</div>
<ScrollArea className="flex-1 min-h-0 max-h-64 pb-2">
<div role="listbox" aria-label="Select time" className="flex flex-col gap-0.5 px-1">
{timeSlots.map(({ hour, minute }) => {
const key = hour * 60 + minute
const active = key === selectedSlotKey
const disabled = isSlotDisabled(hour, minute)
return (
<button
key={key}
ref={el => {
slotRefs.current[key] = el
}}
type="button"
role="option"
aria-selected={active}
disabled={disabled}
onClick={() => selectSlot(hour, minute)}
className={cn(
'h-7 w-full rounded-md px-1.5 text-center text-xs tabular-nums transition-colors',
'hover:bg-accent hover:text-accent-foreground',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',
active &&
'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground shadow-xs',
disabled && 'pointer-events-none opacity-30',
)}
>
{formatSlot(hour, minute)}
</button>
)
})}
</div>
</ScrollArea>
</div>
</div>
<div className="flex items-center gap-1 p-2 border-t">
<Button
type="button"
variant="ghost"
size="sm"
onClick={setNow}
className="h-8 px-2 text-xs"
>
Now
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => commit(undefined)}
className="h-8 px-2 text-xs ml-auto"
>
Clear
</Button>
<Button
type="button"
size="sm"
disabled={!internalDate}
onClick={() => handleOpenChange(false)}
className="h-8 px-2 text-xs"
>
Done
</Button>
</div>
</div>
</PopoverContent>
</Popover>
)
}
export { DateTimePicker }
export type { DateTimePickerProps }
DatePicker
Single (default)
import { DatePicker } from "@/components/ui/date-picker"
const [date, setDate] = React.useState<Date | undefined>()
<DatePicker
selected={date}
onSelect={setDate}
startMonth={new Date(2020, 0)}
endMonth={new Date(2030, 11)}
/>Range
Closes automatically once both from and to are selected:
import { type DateRange } from "@daypicker/react"
const [range, setRange] = React.useState<DateRange | undefined>()
<DatePicker
mode="range"
selected={range}
onSelect={setRange}
startMonth={new Date(2020, 0)}
endMonth={new Date(2030, 11)}
/>Multiple
Stays open — no auto-close when toggling individual dates:
const [dates, setDates] = React.useState<Date[] | undefined>()
<DatePicker
mode="multiple"
selected={dates}
onSelect={setDates}
/>Custom date format
dateFormat is a date-fns format string:
<DatePicker selected={date} onSelect={setDate} dateFormat="MMM dd, yyyy" />Custom trigger
triggerProps forwards to the trigger Button — accepts all Button props except children:
<DatePicker
triggerProps={{ variant: "ghost", className: "w-52 border border-dashed" }}
placeholder="Pick a date..."
selected={date}
onSelect={setDate}
/>Controlled open state
const [open, setOpen] = React.useState(false)
<DatePicker open={open} onOpenChange={setOpen} selected={date} onSelect={setDate} />Popup alignment
<DatePicker align="center" selected={date} onSelect={setDate} />
{/* align: "start" | "center" | "end" — default "start" */}Dropdown navigation bounds
captionLayout="dropdown" is the default. Use startMonth / endMonth to limit the month/year dropdown range:
<DatePicker
startMonth={new Date(2020, 0)}
endMonth={new Date(2030, 11)}
selected={date}
onSelect={setDate}
/>Label caption
<DatePicker captionLayout="label" selected={date} onSelect={setDate} />DateTimePicker
Combines the calendar with a scrollable time slot panel in the same popover. Single mode only. Changing the day preserves the selected time; changing the time slot preserves the selected day.
Basic
import { DateTimePicker } from "@/components/ui/date-picker"
const [date, setDate] = React.useState<Date | undefined>()
<DateTimePicker
selected={date}
onSelect={setDate}
startMonth={new Date(2020, 0)}
endMonth={new Date(2030, 11)}
/>12-hour format
<DateTimePicker
selected={date}
onSelect={setDate}
hourFormat={12}
/>Minute step
Controls the interval between time slots. Defaults to 5:
<DateTimePicker selected={date} onSelect={setDate} minuteStep={15} />
<DateTimePicker selected={date} onSelect={setDate} minuteStep={30} />Allowed values: 1 | 5 | 10 | 15 | 20 | 30.
Custom date format
Defaults to "dd/MM/yyyy HH:mm" (24h) or "dd/MM/yyyy hh:mm a" (12h):
<DateTimePicker selected={date} onSelect={setDate} dateFormat="MMM dd, yyyy HH:mm" />Disable past
Blocks all past dates in the calendar and all past time slots on today:
<DateTimePicker selected={date} onSelect={setDate} disablePast />Date range constraint
<DateTimePicker
selected={date}
onSelect={setDate}
minDate={new Date(2025, 0, 1)}
maxDate={new Date(2025, 11, 31)}
/>Minimum datetime
Disables dates before minTime's calendar day, and disables time slots before minTime on that day. Compose with disablePast — the later constraint wins:
const earliest = addMinutes(new Date(), 30)
<DateTimePicker
selected={date}
onSelect={setDate}
minTime={earliest}
disablePast
/>Now / Clear / Done
The footer always shows three actions:
- Now — snaps selection to the current time (rounded to
minuteStep) - Clear — resets the value to
undefined - Done — closes the popover
Controlled open state
<DateTimePicker open={open} onOpenChange={setOpen} selected={date} onSelect={setDate} />Reference
DatePicker
Prop
Type
DateTimePicker
Prop
Type
Related Components
Autocomplete
A wrapper for Base UI Autocomplete with type-to-filter suggestions, grouped items, async search, inline completion mode, and clear/trigger controls.
Field Wrapper
Opinionated form field wrappers combining Field layout, label, error display, and form controls. Available as library-agnostic, React Hook Form, and TanStack Form variants.