# Date Picker

> A React date picker: type a date in your locale or pick it from a calendar, with presets, limits, optional time and a phone sheet.

- Type: React component (Inputs)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add date-picker`
- Docs: https://wingo-ui.com/components/date-picker

## Interaction

Type a date the way your locale writes it: "09232026" becomes "09/23/2026" as you type (separators appear after each group), "9/23" means this year, a 2 digit year picks the nearest century, ISO "2026-09-23" always works, "+3" is three days from today, and keywords like "today", "tomorrow" or "yesterday" work (other languages through parseKeywords). Clicking the field opens the calendar under it without taking focus from the text; while you type, the calendar follows and highlights the typed day, and a complete, allowed date is committed at once. On blur the text is re-written in the display format; text that does not parse shows invalidDateText, and a day outside min / max or a disabled day shows outOfRangeText. Escape in the field brings back the last good date. Clicking a day (or a preset chip: Today, Tomorrow, Next week or your own) sets it and closes the panel; with withTime a time field sits under the calendar and Done closes. The calendar button toggles the panel; the clear button empties the field. Keyboard: ArrowDown (or Alt+ArrowDown) opens the panel and moves focus to the selected day (else today); arrows move a day or a week, Home / End the week's ends, PageUp / PageDown a month (Shift: a year), Enter or Space picks and returns focus to the field, Escape closes without changing anything. With editable off the field is a button: a click, Enter, Space or ArrowDown open the calendar and a second click closes it. The calendar button opened from the keyboard moves focus to the selected day too. On a phone the field never raises the keyboard: a tap opens a bottom sheet with the title, presets as a row of chips, the calendar with 44px days you can swipe between months, and the time field when withTime (it opens its own wheel sheet); a tap on a day commits and closes, and with withTime or closeOnSelect off the sheet has Done and Clear buttons and dragging it away keeps the old date. name submits the ISO date through a hidden input.

## Installation

```bash
npx wingo-ui@latest add date-picker
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/date-picker` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/date-picker.tsx`

npm dependencies: `npm i @radix-ui/react-popover date-fns motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Calendar](https://wingo-ui.com/components/calendar) ([Markdown](https://wingo-ui.com/components/calendar.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Time Picker](https://wingo-ui.com/components/time-picker) ([Markdown](https://wingo-ui.com/components/time-picker.md))
- [Date utils](https://wingo-ui.com/components/date-utils) ([Markdown](https://wingo-ui.com/components/date-utils.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { useState } from "react"
import { addDays } from "date-fns"
import { DatePicker } from "@/components/ui/date-picker"

export function DeliveryDate() {
  const [date, setDate] = useState<Date | null>(null)
  return (
    <DatePicker
      label="Delivery date"
      description="We deliver Monday to Friday."
      value={date}
      onValueChange={setDate}
      locale="en-US"
      min={addDays(new Date(), 1)}
      disabled={{ dayOfWeek: [0, 6] }}
      presets
      presetLabels={{ nextWeek: "In a week" }}
      invalidDateText="That date does not exist"
      outOfRangeText="We do not deliver on that day"
      sheetTitle="Delivery date"
      name="delivery_date"
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label?` | `ReactNode` | `""` | The field label, linked to the input. Plain text also titles the phone sheet. |
| `description?` | `ReactNode` | `""` | Helper text under the field, linked with aria-describedby. |
| `error?` | `ReactNode` | `""` | Replaces the description with an animated error and marks the field invalid. It wins over invalidDateText and outOfRangeText. |
| `required?` | `boolean` | `false` | The red mark after the label and required on the input. |
| `invalid?` | `boolean` | `false` | The invalid look and aria-invalid without a message. A controlled value outside min / max is flagged the same way. |
| `disabled?` | `boolean or DateMatcher or DateMatcher[] (DateMatcher: Date, Date[], { from, to }, { before }, { after }, { dayOfWeek } or (date) => boolean)` | `false` | true dims the field, blocks typing and never opens the calendar (the hidden input is not submitted). A DateMatcher instead disables those days: a date, a list, { from, to }, { before }, { after }, { dayOfWeek: [0, 6] } for weekends, or a test function. |
| `readOnly?` | `boolean` | `false` | The date stays visible and selectable but can not change: no typing, no calendar, no clear button. |
| `value?` | `Date \| null` |  | The date, controlled (null is empty). With withTime the Date carries the time. A value outside min / max shows as given and is flagged invalid. |
| `defaultValue?` | `Date \| null` | `"null"` | The starting date while uncontrolled. A form reset returns to it. |
| `onValueChange?` | `(date: Date \| null) => void` |  | Called with the new local Date as soon as a complete typed date is allowed, and after a day, a preset, a time, Clear or the sheet's Done. null when cleared. |
| `placeholder?` | `string` | `""` | Empty uses the locale's pattern in lowercase ("dd.mm.yyyy", "mm/dd/yyyy"). Localized forms pass their own, like "zz.ll.aaaa" in Romanian. It also becomes the screen reader format hint. |
| `locale?` | `string` | `""` | How dates are read and written, the month and weekday names and the first day of the week: ro-RO 23.09.2026 from Monday, en-US 09/23/2026 from Sunday. Empty uses the nearest LocaleProvider, else the page's language. |
| `displayFormat?` | `Intl.DateTimeFormatOptions` |  | How the value shows while the field is not focused ({ dateStyle: "long" } gives "March 5, 1990"). Typing is always numeric. Default: 2 digit day and month, numeric year. |
| `editable?` | `boolean` | `true` | Typing on desktop. Off, the field is a button that only opens the calendar (phones always work that way). |
| `autoFormat?` | `boolean` | `true` | Separators appear as digits are typed: "2309" becomes "23.09.". |
| `parseKeywords?` | `Partial<Record<"today" \| "tomorrow" \| "yesterday", string[]>>` |  | Words that mean a day, matched without case or diacritics. English by default; another language passes its own words, e.g. { today: ["hoy"], tomorrow: ["mañana"], yesterday: ["ayer"] }. |
| `min?` | `Date` |  | The first day that can be picked. Earlier days are disabled, navigation stops at its month and a typed earlier date shows outOfRangeText. |
| `max?` | `Date` |  | The last day that can be picked (18 years ago for a birthday). |
| `presets?` | `boolean \| { label: ReactNode; value: Date \| (() => Date); key?: string }[]` | `false` | Chips above the calendar. true gives Today, Tomorrow and Next week; or pass your own. A preset outside the limits is disabled. |
| `presetLabels?` | `Partial<{ today: string; tomorrow: string; nextWeek: string }>` |  | Names of the built-in presets ("Today", "Tomorrow", "Next week"). |
| `clearable?` | `boolean` | `true` | A clear button in the field while it has a date (and Clear in the panel footer and the sheet). |
| `withTime?` | `boolean` | `false` | A time field under the calendar; the value keeps both and the field reads "24.09.2026, 14:30". Picking a day keeps the panel open until Done. Clearing keeps the time for the next pick. |
| `timeStep?` | `number` | `"15"` | Minute step of the time field. |
| `hourCycle?` | `"auto" \| "h12" \| "h23"` | `"auto"` | 12 or 24 hours for the time; auto follows the locale. |
| `timeProps?` | `Partial<TimePickerProps>` |  | Anything else for the time field: labels, min, max, disabledTimes, showNow. |
| `closeOnSelect?` | `boolean` | `true` | Picking a day closes the panel. Never with withTime. Off, the panel (and the sheet) get a Done button. |
| `responsive?` | `boolean` | `true` | Below 768px the field opens a bottom sheet instead of the popover. Off, phones get the popover and can type. |
| `weekStartsOn?` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| -1` | `-1` | The first day of the week: 0 Sunday ... 6 Saturday; -1 follows the locale (Monday in ro-RO, Sunday in en-US). |
| `fixedWeeks?` | `boolean` | `true` | Six weeks in every month, so the panel never changes height while you browse. |
| `monthYearPicker?` | `boolean` | `true` | The calendar caption opens a months grid, then a years grid, for fast jumps (birthdays). |
| `numberOfMonths?` | `number` | `1` | Months side by side in the popover (1 to 3). The phone sheet always shows one. |
| `events?` | `{ date: Date; tone?: Tone; color?: string; label?: string }[]` |  | Dots under days (deliveries, deadlines); labels join the day's aria-label. |
| `calendarProps?` | `Partial<CalendarProps>` |  | Anything else for the calendar: size, radius, strikeDisabled, showWeekNumbers, todayButton, renderDay, labels. |
| `invalidDateText?` | `string` | `"Enter a valid date"` | The error after blur while the text does not parse ("31.02.2026"). |
| `outOfRangeText?` | `string` | `"This date is not available"` | The error after blur for a typed day outside min / max or a disabled day. |
| `defaultOpen?` | `boolean` | `false` | Starts with the calendar (or the sheet) open. Pass open and onOpenChange to control it. |
| `open?` | `boolean` |  | Whether the calendar is open, controlled. It never opens while disabled or readOnly. |
| `onOpenChange?` | `(open: boolean) => void` |  | Called when the popover or sheet opens or closes. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Field heights of 32, 40 and 48px (md is 44px on touch screens), with matching text. |
| `variant?` | `"soft" \| "outline" \| "underline" \| "ghost"` | `"soft"` | The field box: a tinted fill, a border, a bottom line that grows on focus, or nothing until hover. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner radius of the field, scaled per size like the button. Ignored by underline. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The selected day, the chosen preset and the Done button. |
| `color?` | `string` | `""` | Any CSS color for the selection; replaces the tone. |
| `icon?` | `ReactNode` |  | The calendar button's icon. |
| `sheetTitle?` | `string` | `"Select date"` | The phone sheet's title when the label is not plain text (or missing). |
| `labels?` | `Partial<{ clear; open; done; dialog; time; presets; format: string }>` |  | Every other string, merged over the English defaults ("Clear", "Open calendar", "Done", "Choose date", "Time", "Quick picks", "Format: {format}"). format is the screen reader hint after the field. |
| `name?` | `string` | `""` | A hidden input with this name and the local ISO date ("2026-09-23", "2026-09-23T14:30" with withTime), never shifted to UTC. |
| `side?` | `"top" \| "right" \| "bottom" \| "left"` | `"bottom"` | Where the popover opens; it flips when there is no room. |
| `align?` | `"start" \| "center" \| "end"` | `"start"` | How the popover lines up with the field. |
| `container?` | `HTMLElement \| null` |  | Portal target of the popover and the sheet; defaults to document.body. |
| `fieldProps?` | `Partial<FieldProps>` |  | Anything else for the Field wrapper: optional, labelAction, orientation, labelWidth, hideLabel. |
| `id?` | `string` |  | The input's id; generated when left out. |
| `classNames?` | `Partial<Record<"field" \| "label" \| "description" \| "error" \| "counter" \| "box" \| "input" \| "trigger" \| "clear" \| "content" \| "presets" \| "preset" \| "calendar" \| "time" \| "footer", string>>` |  | Extra classes for the inner parts. content is the popover (or the sheet). |
| `className?` | `string` |  | Extra classes for the outermost element: the Field when there is a label or message, else the box. |
| `ref?` | `Ref<HTMLInputElement> (a useRef object or an (el) => void callback)` |  | The text input. Other props (onBlur, autoFocus, aria-*, data-*) land on it too. |

## Parts

### DatePreset (type)

One preset button: { label, value: Date | (() => Date), key? }.

## Examples

- **Birthday**: max 18 years ago, the long display format ("March 5, 1990") and the month / year grid for jumping decades.
- **With time**: withTime adds a time field under the calendar; the field reads the date and time ("mm/dd/yyyy, 2:30 PM") and the hidden input sends the ISO date-time ("yyyy-mm-ddT14:30").
- **Typed formats**: One value in ro-RO (23.09.2026, Monday first) and en-US (09/23/2026, Sunday first). Try 0923, 9/23, ISO, tomorrow or +3.
- **Errors**: A date that does not exist shows invalidDateText after blur, a weekend shows outOfRangeText; the second field has a consumer error.
- **Holidays**: Public holidays disabled and struck through with calendarProps, rounded-square days and a short weekday display.
- **Button only**: editable={false}: a large outline field that only opens the calendar, with a custom teal selection.
- **Custom presets**: Your own quick picks, fixed or computed when the panel opens, in the success tone.
- **Sizes and variants**: sm, md and lg in the four field boxes.

## See also

- [Calendar](https://wingo-ui.com/components/calendar) ([Markdown](https://wingo-ui.com/components/calendar.md))
- [Date Range Picker](https://wingo-ui.com/components/date-range-picker) ([Markdown](https://wingo-ui.com/components/date-range-picker.md))
- [Time Picker](https://wingo-ui.com/components/time-picker) ([Markdown](https://wingo-ui.com/components/time-picker.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))

---

Canonical: https://wingo-ui.com/components/date-picker
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
