Qure UI
Components

Date Picker

A field that opens a calendar, for one date or a span.

'use client'import * as React from 'react'import { DatePicker } from '@/registry/qure/ui/date-picker'export default function DatePickerDemo() {  const [value, setValue] = React.useState<Date | null>(null)  return (    <div className="w-64">      <DatePicker value={value} onValueChange={(v) => setValue(v as Date | null)} />    </div>  )}

Installation

npx shadcn@latest add https://qure-ui.qure.ai/r/date-picker.json

Usage

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.

'use client'import { Input } from '@/registry/qure/ui/input'import { DatePicker } from '@/registry/qure/ui/date-picker'export default function DatePickerSizes() {  return (    <div className="flex w-80 flex-col gap-4">      {/* Beside an Input at the same size, because that is where it lives —          a date field a pixel out in a row of text fields shows. */}      {(['sm', 'md', 'lg'] as const).map((size) => (        <div key={size} className="flex items-center gap-2">          <Input size={size} placeholder="Accession" />          <DatePicker size={size} defaultValue={new Date()} />        </div>      ))}    </div>  )}

A range

'use client'import * as React from 'react'import { type DateRange } from '@/registry/qure/ui/calendar'import { DatePicker } from '@/registry/qure/ui/date-picker'export default function DatePickerRange() {  const [value, setValue] = React.useState<DateRange | null>(null)  return (    <div className="w-80">      {/* The popover stays open until both ends are set — closing on the          first click would make choosing a span take two openings. */}      <DatePicker        mode="range"        placeholder="Study date range"        value={value}        onValueChange={(v) => setValue(v as DateRange | null)}      />    </div>  )}

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

'use client'import { DatePicker } from '@/registry/qure/ui/date-picker'export default function DatePickerStates() {  const today = new Date()  return (    <div className="flex w-64 flex-col gap-4">      <DatePicker placeholder="Empty" />      <DatePicker defaultValue={today} invalid />      <DatePicker defaultValue={today} disabled />      {/* No booking in the past, and not at a weekend. */}      <DatePicker        placeholder="Appointment"        min={today}        disabledDates={(d) => d.getDay() === 0 || d.getDay() === 6}      />    </div>  )}

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.

'use client'import * as React from 'react'import { DatePicker } from '@/registry/qure/ui/date-picker'import { Field, FieldDescription, FieldError, FieldLabel } from '@/registry/qure/ui/field'export default function DatePickerField() {  const [value, setValue] = React.useState<Date | null>(null)  const [touched, setTouched] = React.useState(false)  const error = touched && !value ? 'Choose a date.' : null  return (    <div className="w-72">      <Field className="flex flex-col gap-1.5">        {/* `htmlFor` and the matching `id` do the labelling: a button is a            labelable element, so this is a real association and not an            aria-* approximation of one. */}        <FieldLabel htmlFor="appointment">Appointment</FieldLabel>        <DatePicker          id="appointment"          min={new Date()}          value={value}          onValueChange={(v) => setValue(v as Date | null)}          onBlur={() => setTouched(true)}          invalid={!!error}          /* The one thing still wired by hand. DatePicker sets `aria-invalid`             from `invalid` itself, but it cannot know the id of a message it             does not render. Point at the error only while it exists — an             aria-describedby naming a missing node is skipped inconsistently,             and some screen readers drop the whole attribute with it. */          aria-describedby={            error ? 'appointment-error' : 'appointment-description'          }        />        <FieldDescription id="appointment-description">          Weekdays only, from today.        </FieldDescription>        {error ? (          <FieldError id="appointment-error" match>            {error}          </FieldError>        ) : null}      </Field>    </div>  )}

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:

  • invalid sets aria-invalid as well as data-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.
  • htmlFor plus id labels it properly. A button is a labelable element, so this is a real label association rather than an aria-labelledby approximation.

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

On this page