UIPackage

Date Picker

React date-time
Edit on GitHub

Date input that opens a Calendar in a Popover. Handles parsing, formatting, min/max bounds, and disabled dates. Use the Range Calendar version for from/to selections.

Also available for Vue ->

Installation

$ npx shadcn@latest add https://react.uipkge.dev/r/react/date-picker.json

Or with the named registry: npx shadcn@latest add @uipkge-react/date-picker

Examples

Props

Name Type / Values Default Required
value

Controlled value. ISO `YYYY-MM-DD` string or a `Date`. `null` = empty.

string | Date | null optional
defaultValue

Uncontrolled initial value.

string | Date | null optional
onValueChange (value: string | null) => void optional
placeholder string optional
disabled boolean optional
readOnly boolean optional
clearable boolean optional
format

Format for the trigger label. String presets or Intl.DateTimeFormatOptions for full flexibility.

FormatValue optional
dateFormat

Backward-compat alias for `format`.

FormatValue optional
locale string optional
numberOfMonths number optional
weekStartsOn 0 | 1 | 2 | 3 | 4 | 5 | 6 optional
fixedWeeks boolean optional
minValue string | Date optional
maxValue string | Date optional
status

Validation status — applies colored border to the trigger.

DatePickerStatus optional
size

Size variant of the trigger input.

DatePickerSize optional
placement

Placement of the popover relative to the trigger.

DatePickerPlacement optional
disabledDate

Function to determine if a specific date should be disabled.

(current: Date) => boolean optional
triggerClassName string optional
className string optional

Dependencies

Files (2)

  • components/ui/date-picker/date-picker.tsx 7.2 kB
    'use client'
    
    import * as React from 'react'
    import { Calendar as CalendarIcon, X } from 'lucide-react'
    import { Button } from '@/components/ui/button'
    import { Calendar } from '@/components/ui/calendar'
    import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
    import { cn } from '@/lib/utils'
    
    export type DatePickerStatus = 'error' | 'warning'
    export type DatePickerSize = 'small' | 'middle' | 'large'
    export type DatePickerPlacement =
      | 'top'
      | 'bottom'
      | 'left'
      | 'right'
      | 'topLeft'
      | 'topRight'
      | 'bottomLeft'
      | 'bottomRight'
    
    type FormatValue = 'short' | 'medium' | 'long' | 'full' | Intl.DateTimeFormatOptions
    
    export interface DatePickerProps {
      /** Controlled value. ISO `YYYY-MM-DD` string or a `Date`. `null` = empty. */
      value?: string | Date | null
      /** Uncontrolled initial value. */
      defaultValue?: string | Date | null
      onValueChange?: (value: string | null) => void
      placeholder?: string
      disabled?: boolean
      readOnly?: boolean
      clearable?: boolean
      /** Format for the trigger label. String presets or Intl.DateTimeFormatOptions for full flexibility. */
      format?: FormatValue
      /** Backward-compat alias for `format`. */
      dateFormat?: FormatValue
      locale?: string
      numberOfMonths?: number
      weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6
      fixedWeeks?: boolean
      minValue?: string | Date
      maxValue?: string | Date
      /** Validation status — applies colored border to the trigger. */
      status?: DatePickerStatus
      /** Size variant of the trigger input. */
      size?: DatePickerSize
      /** Placement of the popover relative to the trigger. */
      placement?: DatePickerPlacement
      /** Function to determine if a specific date should be disabled. */
      disabledDate?: (current: Date) => boolean
      triggerClassName?: string
      className?: string
    }
    
    /** Parse an ISO `YYYY-MM-DD` (or full ISO) string / Date into a local Date. */
    function coerce(v: string | Date | null | undefined): Date | null {
      if (!v) return null
      if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v
      const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(v)
      if (m) {
        const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]))
        return Number.isNaN(d.getTime()) ? null : d
      }
      const d = new Date(v)
      return Number.isNaN(d.getTime()) ? null : d
    }
    
    /** Serialize a Date back to an ISO `YYYY-MM-DD` string (local, no time component). */
    function toISODate(d: Date): string {
      const y = d.getFullYear()
      const mo = String(d.getMonth() + 1).padStart(2, '0')
      const da = String(d.getDate()).padStart(2, '0')
      return `${y}-${mo}-${da}`
    }
    
    const DatePicker = React.forwardRef<HTMLButtonElement, DatePickerProps>(
      (
        {
          value,
          defaultValue = null,
          onValueChange,
          placeholder = 'Pick a date',
          disabled = false,
          readOnly = false,
          clearable = true,
          format = 'medium',
          dateFormat,
          locale = 'en-US',
          numberOfMonths = 1,
          weekStartsOn = 0,
          fixedWeeks = false,
          minValue,
          maxValue,
          status,
          size = 'middle',
          placement = 'bottomLeft',
          disabledDate,
          triggerClassName,
          className,
        },
        ref,
      ) => {
        const [open, setOpen] = React.useState(false)
    
        const isControlled = value !== undefined
        const [internal, setInternal] = React.useState<Date | null>(() => coerce(defaultValue))
        const selected = isControlled ? coerce(value) : internal
    
        const effectiveFormat = dateFormat ?? format
    
        const minDate = React.useMemo(() => coerce(minValue) ?? undefined, [minValue])
        const maxDate = React.useMemo(() => coerce(maxValue) ?? undefined, [maxValue])
    
        function fmtDate(d: Date) {
          const opts: Intl.DateTimeFormatOptions =
            typeof effectiveFormat === 'object' ? effectiveFormat : { dateStyle: effectiveFormat }
          return new Intl.DateTimeFormat(locale, opts).format(d)
        }
    
        const display = selected ? fmtDate(selected) : ''
        const hasValue = Boolean(selected)
    
        function commit(d: Date | null) {
          if (!isControlled) setInternal(d)
          onValueChange?.(d ? toISODate(d) : null)
        }
    
        function handleSelect(d: Date | undefined) {
          commit(d ?? null)
          if (d) setOpen(false)
        }
    
        function clear(event: React.MouseEvent) {
          event.stopPropagation()
          if (disabled || readOnly) return
          commit(null)
        }
    
        const placementMap: Record<
          DatePickerPlacement,
          { side: 'top' | 'bottom' | 'left' | 'right'; align: 'start' | 'center' | 'end' }
        > = {
          top: { side: 'top', align: 'center' },
          bottom: { side: 'bottom', align: 'center' },
          left: { side: 'left', align: 'center' },
          right: { side: 'right', align: 'center' },
          topLeft: { side: 'top', align: 'start' },
          topRight: { side: 'top', align: 'end' },
          bottomLeft: { side: 'bottom', align: 'start' },
          bottomRight: { side: 'bottom', align: 'end' },
        }
        const popoverPlacement = placementMap[placement] ?? { side: 'bottom', align: 'start' }
    
        const buttonSize = size === 'small' ? 'sm' : size === 'large' ? 'lg' : 'default'
    
        const triggerClasses = cn(
          'min-w-[240px]',
          'justify-start gap-2 text-left font-normal',
          !hasValue && 'text-muted-foreground',
          status === 'error' && 'border-destructive focus-visible:ring-destructive',
          status === 'warning' && 'border-warning focus-visible:ring-warning',
          triggerClassName,
          className,
        )
    
        const calendarDisabled = React.useMemo(() => {
          const matchers: ((d: Date) => boolean)[] = []
          if (minDate) matchers.push((d) => d < minDate)
          if (maxDate) matchers.push((d) => d > maxDate)
          if (disabledDate) matchers.push(disabledDate)
          return matchers.length ? (d: Date) => matchers.some((m) => m(d)) : undefined
        }, [minDate, maxDate, disabledDate])
    
        return (
          <Popover open={open} onOpenChange={setOpen}>
            <PopoverTrigger asChild>
              <Button
                ref={ref}
                type="button"
                variant="outline"
                size={buttonSize}
                disabled={disabled}
                className={triggerClasses}
                data-uipkge=""
                data-slot="date-picker"
              >
                <CalendarIcon className="size-4" aria-hidden="true" />
                <span className="flex-1 truncate">{display || placeholder}</span>
                {clearable && hasValue && !disabled && !readOnly && (
                  <button
                    type="button"
                    className="text-muted-foreground hover:text-foreground focus-visible:ring-ring -mr-1 inline-flex size-9 items-center justify-center rounded transition-colors focus-visible:ring-2 focus-visible:outline-none"
                    aria-label="Clear date"
                    onClick={clear}
                  >
                    <X className="size-3.5" />
                  </button>
                )}
              </Button>
            </PopoverTrigger>
            <PopoverContent className="w-auto p-0" side={popoverPlacement.side} align={popoverPlacement.align}>
              <Calendar
                mode="single"
                selected={selected ?? undefined}
                onSelect={handleSelect}
                numberOfMonths={numberOfMonths}
                weekStartsOn={weekStartsOn}
                fixedWeeks={fixedWeeks}
                disabled={calendarDisabled}
              />
            </PopoverContent>
          </Popover>
        )
      },
    )
    DatePicker.displayName = 'DatePicker'
    
    export { DatePicker }
  • components/ui/date-picker/index.ts 0.1 kB
    export {
      DatePicker,
      type DatePickerProps,
      type DatePickerStatus,
      type DatePickerSize,
      type DatePickerPlacement,
    } from './date-picker'

Raw manifest: https://react.uipkge.dev/r/react/date-picker.json