Date picker
Picks a day, typed in the field or chosen from a calendar under it. Holds a day with no time, like a Date field.
Installation
The CLI installs the Radix or Base UI file to match your project, with what it builds on: button, calendar, input-group.
If your project doesn't have Wisty UI's theme yet, add it once with npx shadcn@latest add @wisty/cosmos. Until then, components keep shadcn's default look. Installation has the details.
When to use
Use it
- People enter one day in a form, like a close date, a due date or a birthday.
- People type the date or pick it from a calendar under the field. The field shows it in the locale's style, "Jun 18, 2026" by default, and
dateStylechanges it. - The value is a day with no time and no time zone, like a Salesforce Date field. The date picker holds it as
"YYYY-MM-DD". - Some days can't be picked, like dates before today. Pass
minandmax. - It replaces
lightning-inputwithtype="date".
Don't use it
- The value is a date and a time, like a Salesforce Date/Time field. The date picker holds a day only.
- People pick a range, like a reporting period. Use two date pickers, each bounding the other with
minandmax, as the reporting period example does. - The calendar is always on the page, not in a field. Use Calendar.
- A button opens the calendar, as in Salesforce's React template. Compose Calendar in a Popover. People type in the date picker's field.
- The date is only read. Show it as text.
Usage
import {
DatePicker,
DatePickerCalendar,
DatePickerContent,
DatePickerControl,
DatePickerInput,
DatePickerToday,
DatePickerTrigger,
} from "@/components/wisty/date-picker"DatePicker
├── DatePickerControl
│ ├── DatePickerInput
│ └── DatePickerTrigger
└── DatePickerContent
├── DatePickerCalendar
└── DatePickerTodayDatePicker holds the value, the typed text and the calendar's state; the other parts only draw. onValueChange gets the day, or null with the reason when the text isn't a date the field accepts. Keep the reason to show a message:
"use client"
import { useState } from "react"
import { type DatePickerError } from "@/components/wisty/date-picker"export function CloseDate() {
const [closeDate, setCloseDate] = useState<string | null>(null)
const [error, setError] = useState<DatePickerError>()
return (
<DatePicker
name="CloseDate"
value={closeDate}
onValueChange={(value, details) => {
setCloseDate(value)
setError(details.error)
}}
>
<DatePickerControl>
<DatePickerInput aria-label="Close date" />
<DatePickerTrigger />
</DatePickerControl>
<DatePickerContent>
<DatePickerCalendar />
<DatePickerToday />
</DatePickerContent>
</DatePicker>
)
}The field reads "YYYY-MM-DD", the locale's numeric order (10/12/2026 in en-US) and a month name with a day and a year (Oct 12, 2026; Sept works too), in the locale's own digits; a two-digit year is in the 2000s. error is "format" for text it can't read, and "min" or "max" for a day outside the bounds. In a form, name submits the day as "YYYY-MM-DD". To accept other text, like "next Friday", pass your own parse.
In Radix projects, the calendar's popover is still built on @base-ui/react, so installing Date picker adds that package beside radix-ui. The two work side by side.
Examples
Min and max
Days outside `min` and `max` are disabled, the year picklist stops at their years, and a typed day outside them is an error.
From today to the end of the quarter.
States
A required field showing the error `onValueChange` reports (empty or bad text), and a `disabled` one holding a date.
Reporting period
Two date pickers for a period, each bounding the other with `min` and `max`: there's no range picker.
In a dialog
A due date in a modal dialog: the calendar opens over it, and Escape closes the calendar first, then the dialog.
API reference
| Part | Built on |
|---|---|
DatePicker | |
DatePickerControl | InputGroup |
DatePickerInput | <input> (opens in a new tab) |
DatePickerTrigger | InputGroupButton |
DatePickerContent | Base UI Popover.Popup (opens in a new tab) |
DatePickerCalendar | Calendar |
DatePickerToday | Button |
DatePicker
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | null | The day, as "YYYY-MM-DD", or null when the field is empty or its text isn't accepted. | |
defaultValue | string | null | null | |
onValueChange | ( value: string | null, details: DatePickerChangeDetails ) => void | ||
min | string | The earliest day people can pick or type, as "YYYY-MM-DD". | |
max | string | The latest day people can pick or type, as "YYYY-MM-DD". | |
today | string | The day the calendar marks as today, as "YYYY-MM-DD". Default the browser's date. | |
locale | string | "en-US" | Formats and reads dates. |
dateStyle | "short" | "medium" | "long" | "medium" | How the field shows the date. |
parse | DatePickerParse | parseDate | Reads typed text into "YYYY-MM-DD", or null when it isn't a date. |
name | string | Submits the value as "YYYY-MM-DD" with a form, from a hidden input. | |
required | boolean | false | |
disabled | boolean | false | |
readOnly | boolean | false |
DatePickerCalendar
| Prop | Type | Default | Description |
|---|---|---|---|
captionLayout | "label" | "dropdown" | "dropdown-months" | "dropdown-years" | "dropdown-years" | Show dropdowns to navigate between months or years. |
DatePickerToday
| Prop | Type | Default |
|---|---|---|
children | ReactNode | "Today" |
parseDatefunction
Reads a typed date: "YYYY-MM-DD", the locale's numeric order, or a month name (cut short too) with a day and a year, in the locale's digits. A two-digit year is in the 2000s. Returns "YYYY-MM-DD", or null when it isn't a date.
| Parameter | Type | Default |
|---|---|---|
text* | string | |
__1* | { locale: string } |
Returns string | null.
DatePickerErrortype
type DatePickerError = "format" | "min" | "max"DatePickerChangeDetailstype
| Member | Type | Default | Description |
|---|---|---|---|
text* | string | The field's text when the value changed. | |
error | DatePickerError | Why the text isn't a date the field accepts. Unset when it is. |
DatePickerParsetype
type DatePickerParse = (
text: string,
options: { locale: string }
) => string | nullSLDS mapping
The date picker is SLDS's datepicker as lightning-input draws it. DatePickerControl is the form element's control with an icon on the right (slds-input-has-icon_right), DatePickerInput is slds-input, and DatePickerTrigger is the icon button inside it (slds-input__icon_right). DatePickerContent is slds-datepicker in a dropdown under the field's start edge (slds-dropdown slds-dropdown_left). DatePickerCalendar draws its header (slds-datepicker__filter: the month between the arrows, then the year picklist) and its grid (slds-datepicker__month). DatePickerToday is the Today link (slds-text-link).
Known gaps:
- SLDS swaps in the phone's own date input on mobile. The date picker keeps its field and calendar.
- No "date format visible" help under the field.
- The calendar opens right under the field, over help or error text. SLDS opens it under the whole form element.
- The year picklist's arrow is a chevron, where SLDS draws two arrows, and the calendar icon is lucide's, where SLDS draws
utility:event. - The text the date picker adds ("Select a date", "Today", "today" and "selected" in each day's name) is English. Month and weekday names follow
locale.
| Prop | Value | SLDS | Notes |
|---|---|---|---|
(no value) | default (added) | slds-form-element slds-dropdown-trigger, slds-input-has-icon_right, slds-input__icon_right, slds-datepicker slds-dropdown slds-dropdown_left | A typed field with the calendar button on the right, and the calendar under the field's start edge. |
dateStyle | short, medium, long (added) | lightning-input date-style | How the field shows the day: 6/18/26, Jun 18, 2026 (the default) or June 18, 2026, in en-US. |
locale | (a locale) (added) | lightning-input's user locale | The field's format and what it reads, and the calendar's names, digits and first day of the week. Defaults to en-US. |
min, max | (a day) (added) | slds-disabled-text (days), slds-has-error | Days outside are disabled, the year picklist runs from min's year to max's, and a typed day outside is an error. |
captionLayout | dropdown-years (added) | slds-datepicker__filter, slds-datepicker__filter_month, slds-select | On DatePickerCalendar, the default: the month between the arrows and the year picklist after them. |
aria-invalid | true (added) | slds-has-error | Set by the date picker for text it can't read or a day outside the bounds, or by the caller. |
disabled | true (added) | slds-input[disabled], slds-button_icon[disabled] |
Keyboard interactions
| Keys | Action |
|---|---|
| Enter or Space on the calendar icon | Opens the calendar with focus on the picked day, or today. |
| Down Arrow in the field | Opens the calendar the same way. |
| Enter in the field | Reads the typed text and shows it in the field's style, or marks it invalid. Leaving the field does the same. |
| Arrow keys | Move a day, or a week. |
| Home, End | Move to the start or end of the week. |
| Page Up, Page Down | Move a month. With Shift, a year. |
| Enter or Space on a day | Picks it, closes the calendar and returns focus to the field. |
| Tab, Shift+Tab | Move between the arrows, the year picklist, the days and Today. Focus stays in the calendar. |
| Escape | Closes the calendar and returns focus to the field. Inside a modal dialog, the first Escape closes only it. |
A click in the field opens the calendar and keeps focus there, so people can type.
Accessibility
- Name the field with a
FieldLabelwhosehtmlForisDatePickerInput'sid, or passaria-labelto it. - For an error you decide, like an empty required field, set
aria-invalidonDatePickerInputand pointaria-describedbyat the message. The date picker setsaria-invaliditself, and the input's validity, for text it can't read, days outsideminandmax, and avaluethat isn't a day. - In Lightning Blue, today's day in the calendar is 3.67:1, and in Cosmos dark the error text is about 4.3:1, both below WCAG AA's 4.5:1. SLDS's own stylesheet does the same.
Responsive behavior
- The field fills its parent's width. Set the width on the parent or the Field.
- The calendar is 280px wide. It opens below the field, or above when there's no room below, and never beside it.
Rules
- Keep the value a
"YYYY-MM-DD"string. ADateor a timestamp can land on the day before in another time zone. - Pass a
valuethat names a real day. One that doesn't, like"2023-02-29", shows as it is with a"format"error, selects nothing in the calendar and submits nothing; it's never rolled to the next day, andonValueChangeisn't called for it. - Changing
localeordateStyleshows the value again in the new style. Text typed and not yet committed is replaced, never read in the new locale. - Disabled, the date picker submits nothing with a form, as a disabled input doesn't.
- Keep
DatePickerInputandDatePickerTriggerinsideDatePickerControl, andDatePickerCalendarinsideDatePickerContent: the calendar opens under the control, and the root wires them together. - Never pass
asChildorrender.
Related
- CalendarShows a month of days to pick a date or a range.
- Input groupPuts icons, text or buttons inside an input or textarea.
- FieldLays out a form control with its label, help text and error.
- PopoverShows details or a short form in a panel anchored to a trigger.
- Time pickerPicks a time of day, typed in the field or chosen from a list of times.
- Datetime pickerPicks a date and a time, typed or chosen from a calendar and a list.