Date Picker
A field that opens a calendar, for one date or a span.
Installation
npx shadcn@latest add https://qure-ui.qure.ai/r/date-picker.jsonUsage
import { DatePicker } from "@/components/ui/date-picker"const [value, setValue] = React.useState<Date | null>(null)
<DatePicker value={value} onValueChange={(v) => setValue(v as Date | null)} />The trigger borrows Input's geometry exactly — same heights, radius, border and focus ring — because a date field lives in a row of text fields and a pixel out shows.
A range
The popover stays open until both ends are set. Closing on the first click would make choosing a span take two openings, which is the most common thing to get wrong here.
States
min, max and disabledDates pass through to the Calendar, so
the same rules that grey a day out there apply here.
Typing is not supported, on purpose
The trigger is a button, not a text input.
Parsing a typed date correctly across locales is its own project — 03/04 is the 3rd of April
to half the world and the 4th of March to the other half, and a parser that is nearly right
produces the wrong day without ever saying so. In clinical software that is not an acceptable
class of bug to ship by accident.
Where typing genuinely matters — a date of birth, where reaching back thirty years through a month grid is miserable — compose it yourself and own the parsing:
<Popover>
<div className="flex gap-2">
<Input value={text} onChange={(e) => { setText(e.target.value); setDate(parse(e.target.value)) }} />
<PopoverTrigger render={<Button variant="tertiary" size="icon-md" aria-label="Open calendar" />}>
<CalendarIcon />
</PopoverTrigger>
</div>
<PopoverContent><Calendar value={date} onValueChange={setDate} /></PopoverContent>
</Popover>That way the format your users type is a decision your product makes, rather than one this component guessed.
In a Field
Weekdays only, from today.
The trigger is a Popover button rather than a Base UI form control, so — unlike Input,
Select or TimeField — it does not register itself with a surrounding
Field, and a Field around it cannot wire it up on its own.
Two of the three attributes are handled for you:
invalidsetsaria-invalidas well asdata-invalid. The first is the announcement and shows nothing; the second is what the stylesheet recolours from and says nothing to a reader who cannot see it. They were two separate props and it was too easy to pass one, shipping an error that was visible or audible but not both.htmlForplusidlabels it properly. Abuttonis a labelable element, so this is a real label association rather than anaria-labelledbyapproximation.
What is left is aria-describedby, because the component cannot know the id of a message it
does not render:
<DatePicker
id="appointment"
invalid={!!error}
aria-describedby={error ? 'appointment-error' : 'appointment-description'}
/>Point aria-describedby at an id only while that element is on the page. An
aria-describedby naming a node that does not exist is not ignored consistently — some screen
readers discard the whole attribute, taking the description with it.
Clearing
When there is a date and clearable is on, a clear affordance appears in the trigger.
It is a span with role="button", not a real <button> — a button nested inside the popover
trigger is a button inside a button, which is invalid HTML that browsers resolve by silently
dropping one of them. Because it cannot be tabbed to, the keyboard route is
Backspace or Delete on the focused trigger.
If you build your own trigger, keep a keyboard path to clearing. A clear button that only works with a mouse is the most common accessibility defect in date fields, and it is invisible in a screenshot.
Formatting
The displayed date comes from Intl, so it follows the reader's locale unless you say
otherwise:
<DatePicker formatOptions={{ dateStyle: "full" }} />
<DatePicker locale="en-GB" formatOptions={{ day: "2-digit", month: "short", year: "numeric" }} />API Reference
Prop
Type