ComponentsForm
Date picker
A field you can type a date into, with a calendar that opens below it.
Import
import { DatePicker } from "@booleanpress/ui/date-picker"Also install react-day-picker: pnpm add react-day-picker
Usage
DatePicker is an Input with the package's Calendar in a Popover. It needs the react-day-picker peer package. Name it with a Label whose htmlFor is its id: id, placeholder, aria-* and the other input attributes go on the text field, and className on the root, which holds the field and its button.
import { useState } from "react"
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
export function SendOn() {
const [date, setDate] = useState<Date | null>(null)
return (
<div className="flex flex-col gap-2">
<Label htmlFor="send-on">Send on</Label>
<DatePicker id="send-on" value={date} onValueChange={setDate} />
</div>
)
}It is uncontrolled with defaultValue, or controlled with value and onValueChange. The value is a Date or null; with mode="multiple" it is a Date[] and the field lists the dates with commas (with semicolons when a date's own text has a comma, as "EEE, d MMM yyyy" does). The field shows the date in the provider's locale ("10/14/2026" in en-US, "14.10.2026" in de-DE), or in format, a pattern of yyyy yy MMMM MMM MM M dd d EEE HH hh mm a and literal text.
Typing. The text is read when the field loses focus and on Enter: numbers in the locale's order, a month name, eight digits with no separators, or the format pattern with any separator. A year of one or two digits is the year nearest today, within 50 years (85 is 1985, 30 is 2030 in 2026); with showTime, AM or PM may stand before or after the time, as the locale writes it. Text the person has not changed leaves the value as it is. Text that is not a date, or a date outside min and max, stays in the field so it can be corrected, marks the field invalid (aria-invalid), and empties the value until it is fixed. The field never throws the typed text away, and the value never holds a date the field does not show.
The calendar. trigger chooses what opens it: button (the default) joins a calendar button to the field's end, icon puts the icon inside the field, and field opens it on a click in the field. Alt+ArrowDown opens it from the field in every case. showButtonBar adds Today and Clear under it; showTime adds hours and minutes (12 or 24 h by locale, or hourCycle); view="month" and view="year" choose a month or a year instead of a day; numberOfMonths shows several months; inline shows the calendar on the page with no field. min and max block the days outside them and stop the month arrows there; with showTime, a min or max that has a time (any but midnight) also bounds the time on its day, and a day or time chosen in the popup is brought inside it. Today chooses today's day, or its month's 1st with view="month", or its 1 January with view="year", and shows its month. calendarProps passes modifiers, modifiersClassNames, disabled days, labels and other props to the Calendar, for example to mark days with a dot.
Time zones. Values are plain Date instants. A day is midnight of that day in the provider's timeZone (the browser's when none is set), and the calendar, the field's text and the typed text all use that zone. On a site whose timeZone is "Europe/Berlin", a visitor in New York who picks 14 October gets the instant of 14 October, 00:00 in Berlin (13 October, 18:00 in New York): the date the site means, whatever the visitor's clock says. To send it to a server, give the picker a name: a hidden input carries 2026-10-14 (the full ISO instant with showTime), already in the site's zone. Read a Date back in the same zone. A day whose midnight the clocks skip (the change to summer time happens at midnight in some zones, such as America/Santiago) is the first instant of that day. On a page rendered on a server, set the provider's locale and timeZone, so the server and the browser write the same text.
size (sm 28 px, default 35 px, lg 42 px) and variant="filled" follow Input; fluid fills the column; clearable adds a ×. Pass today and defaultMonth for screenshots and tests that must not read the clock.
Examples
Basic
A named field with a date in the locale's format and the calendar button joined to its end.
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
export default function DatePickerBasic() {
return (
<div className="flex flex-col gap-2">
<Label htmlFor="send-on">Send the newsletter on</Label>
<DatePicker id="send-on" defaultValue={new Date(2026, 9, 14)} today={new Date(2026, 9, 3)} />
</div>
)
}Format
format="dd/MM/yyyy" and format="EEE, d MMM yyyy": typed text is read with the same pattern.
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
export default function DatePickerFormat() {
return (
<div className="flex flex-wrap gap-6">
<div className="flex flex-col gap-2">
<Label htmlFor="invoice-date">Invoice date</Label>
<DatePicker id="invoice-date" format="dd/MM/yyyy" defaultValue={new Date(2026, 9, 14)} today={new Date(2026, 9, 3)} />
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="report-date">Report date</Label>
<DatePicker
id="report-date"
format="EEE, d MMM yyyy"
defaultValue={new Date(2026, 9, 14)}
today={new Date(2026, 9, 3)}
className="w-56"
/>
</div>
</div>
)
}Icon trigger
trigger="button", trigger="icon" with the calendar icon or your own icon, and trigger="field", where a click in the field opens the calendar.
import { ClockIcon } from "lucide-react"
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
const today = new Date(2026, 9, 3)
export default function DatePickerIcon() {
return (
<div className="grid w-full max-w-xl gap-6 sm:grid-cols-2">
<div className="flex flex-col gap-2">
<Label htmlFor="trigger-button">Button</Label>
<DatePicker id="trigger-button" trigger="button" today={today} fluid />
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="trigger-icon">Icon in the field</Label>
<DatePicker id="trigger-icon" trigger="icon" today={today} fluid />
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="trigger-custom">Custom icon</Label>
<DatePicker id="trigger-custom" trigger="icon" icon={<ClockIcon aria-hidden="true" />} today={today} fluid />
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="trigger-field">The field opens it</Label>
<DatePicker id="trigger-field" trigger="field" today={today} fluid />
</div>
</div>
)
}Min and max
min and max block the days outside 5–25 October and stop the month arrows; a typed date outside them is invalid.
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
export default function DatePickerMinMax() {
return (
<div className="flex flex-col gap-2">
<Label htmlFor="campaign-start">Campaign start (5 to 25 October)</Label>
<DatePicker
id="campaign-start"
min={new Date(2026, 9, 5)}
max={new Date(2026, 9, 25)}
defaultMonth={new Date(2026, 9)}
today={new Date(2026, 9, 3)}
/>
</div>
)
}Multiple dates
mode="multiple" toggles days on and off; the field lists them, and typed dates are separated by commas.
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
export default function DatePickerMultiple() {
return (
<div className="flex w-full max-w-sm flex-col gap-2">
<Label htmlFor="digest-days">Digest days</Label>
<DatePicker
id="digest-days"
mode="multiple"
defaultValue={[new Date(2026, 9, 6), new Date(2026, 9, 13), new Date(2026, 9, 20)]}
today={new Date(2026, 9, 3)}
fluid
/>
</div>
)
}Button bar
showButtonBar adds Today and Clear under the calendar.
Time
showTime adds hours, minutes and AM/PM under the calendar; hourCycle forces a 12- or 24-hour clock.
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
const today = new Date(2026, 9, 3)
export default function DatePickerTime() {
return (
<div className="flex flex-wrap gap-6">
<div className="flex flex-col gap-2">
<Label htmlFor="send-at-12">Send at (12-hour)</Label>
<DatePicker id="send-at-12" showTime hourCycle={12} defaultValue={new Date(2026, 9, 14, 9, 30)} today={today} className="w-60" />
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="send-at-24">Send at (24-hour)</Label>
<DatePicker id="send-at-24" showTime hourCycle={24} defaultValue={new Date(2026, 9, 14, 17, 45)} today={today} className="w-60" />
</div>
</div>
)
}Month picker
view="month" chooses a month (the value is its 1st); the year in the header opens a year list.
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
export default function DatePickerMonthPicker() {
return (
<div className="flex flex-col gap-2">
<Label htmlFor="billing-month">Billing month</Label>
<DatePicker id="billing-month" view="month" defaultValue={new Date(2026, 9, 1)} today={new Date(2026, 9, 3)} />
</div>
)
}Year picker
view="year" chooses a year from a decade (the value is its 1 January).
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
export default function DatePickerYearPicker() {
return (
<div className="flex flex-col gap-2">
<Label htmlFor="report-year">Annual report year</Label>
<DatePicker id="report-year" view="year" defaultValue={new Date(2026, 0, 1)} today={new Date(2026, 9, 3)} />
</div>
)
}Two months
numberOfMonths={2} shows October and November side by side, divided by a line.
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
export default function DatePickerTwoMonths() {
return (
<div className="flex flex-col gap-2">
<Label htmlFor="renewal-date">Renewal date</Label>
<DatePicker id="renewal-date" numberOfMonths={2} defaultMonth={new Date(2026, 9)} today={new Date(2026, 9, 3)} />
</div>
)
}Date template
calendarProps adds a delivery modifier: a dot under the days with deliveries, and the same fact in each day's accessible name.
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
// Days with scheduled deliveries get a dot, and say so to screen readers in the day's name.
const deliveries = [2, 6, 9, 13, 16, 22, 27, 29].map((day) => new Date(2026, 9, day))
export default function DatePickerDateTemplate() {
return (
<div className="flex flex-col gap-2">
<Label htmlFor="delivery-day">Delivery day</Label>
<DatePicker
id="delivery-day"
defaultMonth={new Date(2026, 9)}
today={new Date(2026, 9, 3)}
calendarProps={{
modifiers: { delivery: deliveries },
modifiersClassNames: {
delivery: "after:pointer-events-none after:absolute after:inset-x-0 after:bottom-1 after:mx-auto after:size-1 after:rounded-full after:bg-info",
},
// Your own day names replace the built-in ones, so they say "Today" and "selected" themselves.
labels: {
labelDayButton: (date, modifiers) =>
[
modifiers.today ? "Today" : "",
date.toLocaleDateString("en-US", { weekday: "long", day: "numeric", month: "long", year: "numeric" }),
modifiers.delivery ? "deliveries scheduled" : "",
modifiers.selected ? "selected" : "",
]
.filter(Boolean)
.join(", "),
},
}}
/>
</div>
)
}Inline
inline shows the calendar on the page, named by aria-label, with no field or popup.
import { useState } from "react"
import { DatePicker } from "@booleanpress/ui/date-picker"
export default function DatePickerInline() {
const [date, setDate] = useState<Date | null>(new Date(2026, 9, 14))
return (
<div className="flex flex-col items-start gap-3">
<DatePicker inline aria-label="Publish date" value={date} onValueChange={setDate} today={new Date(2026, 9, 3)} />
<p className="text-sm text-muted-foreground">
Publishes on {date ? date.toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric" }) : "no date yet"}.
</p>
</div>
)
}Clear
clearable shows a × while there is a date; it empties the value and keeps the focus in the field.
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
export default function DatePickerClear() {
return (
<div className="flex flex-col gap-2">
<Label htmlFor="expires-on">API key expires on</Label>
<DatePicker id="expires-on" clearable defaultValue={new Date(2026, 11, 31)} today={new Date(2026, 9, 3)} />
</div>
)
}Sizes
sm is 28 px tall with a 28 px button, default 35 px with 36 px, lg 42 px with 42 px.
import { DatePicker } from "@booleanpress/ui/date-picker"
const today = new Date(2026, 9, 3)
export default function DatePickerSizes() {
return (
<div className="flex flex-col items-center gap-3">
<DatePicker size="sm" aria-label="Small" placeholder="Small" today={today} />
<DatePicker aria-label="Default" placeholder="Default" today={today} />
<DatePicker size="lg" aria-label="Large" placeholder="Large" today={today} />
</div>
)
}Filled
variant="filled" draws the grey --field-filled fill, which stays on hover and focus.
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
export default function DatePickerFilled() {
return (
<div className="flex flex-col gap-2">
<Label htmlFor="ticket-due">Ticket due</Label>
<DatePicker id="ticket-due" variant="filled" defaultValue={new Date(2026, 9, 21)} today={new Date(2026, 9, 3)} />
</div>
)
}Fluid
fluid makes the field and its button fill the width of their container.
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
export default function DatePickerFluid() {
return (
<div className="flex w-full flex-col gap-2">
<Label htmlFor="trial-ends">Trial ends</Label>
<DatePicker id="trial-ends" fluid defaultValue={new Date(2026, 9, 31)} today={new Date(2026, 9, 3)} />
</div>
)
}Disabled
Neither the field nor the button can be used.
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
export default function DatePickerDisabled() {
return (
<div className="flex flex-col gap-2">
<Label htmlFor="created-on">Created on</Label>
<DatePicker id="created-on" disabled defaultValue={new Date(2026, 9, 1)} today={new Date(2026, 9, 3)} />
</div>
)
}Invalid
aria-invalid draws the error edge; aria-describedby reads the message. Text that is not a date sets the same state.
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
export default function DatePickerInvalid() {
return (
<div className="flex flex-col gap-2">
<Label htmlFor="go-live">Go-live date</Label>
<DatePicker id="go-live" aria-invalid aria-describedby="go-live-error" today={new Date(2026, 9, 3)} />
<p id="go-live-error" className="text-sm text-destructive-strong">
Choose the day the mailer goes live.
</p>
</div>
)
}With a float label
Inside FloatLabel, the label rests in the empty field and moves above it once there is a date.
import { DatePicker } from "@booleanpress/ui/date-picker"
import { FloatLabel } from "@booleanpress/ui/float-label"
import { Label } from "@booleanpress/ui/label"
export default function DatePickerFloatLabel() {
return (
<div className="flex flex-col gap-8 pt-4.5">
<FloatLabel>
<DatePicker id="fl-renewal" today={new Date(2026, 9, 3)} />
<Label htmlFor="fl-renewal">Renewal date</Label>
</FloatLabel>
<FloatLabel>
<DatePicker id="fl-started" defaultValue={new Date(2026, 9, 1)} today={new Date(2026, 9, 3)} />
<Label htmlFor="fl-started">Plan started</Label>
</FloatLabel>
</div>
)
}With a form
The picker sits outside its form and belongs to it through form; the form's Reset puts defaultValue back, in the field's text too.
import * as React from "react"
import { Button } from "@booleanpress/ui/button"
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
export default function DatePickerWithForm() {
const [sent, setSent] = React.useState("")
return (
<div className="flex w-full max-w-sm flex-col gap-3">
{/* The picker sits outside the form and belongs to it through `form`. */}
<div className="flex flex-col gap-2">
<Label htmlFor="schedule-day">Send on</Label>
<DatePicker id="schedule-day" name="day" form="schedule" defaultValue={new Date(2026, 9, 5)} />
</div>
<form
id="schedule"
className="flex gap-2 rounded-md border p-3"
onSubmit={(event) => {
event.preventDefault()
setSent([...new FormData(event.currentTarget)].map(([key, value]) => `${key} = ${value}`).join(", "))
}}
>
<Button type="submit">Submit</Button>
<Button type="reset" variant="outline">
Reset
</Button>
</form>
<pre className="overflow-x-auto rounded-md bg-muted px-3 py-2 font-mono text-xs whitespace-pre-wrap text-foreground">{sent ? `Sent: ${sent}` : "Press Submit to see what the form sends."}</pre>
</div>
)
}Another locale
format="d MMMM yyyy" in Bengali and Hindi: the month names the locale writes are read back as typed.
import { BooleanUIProvider } from "@booleanpress/ui/provider"
import { DatePicker } from "@booleanpress/ui/date-picker"
import { Label } from "@booleanpress/ui/label"
export default function DatePickerAnotherLocale() {
return (
<div className="flex flex-col gap-4">
<BooleanUIProvider locale="bn-BD">
<div className="flex flex-col gap-2">
<Label htmlFor="send-on-bn">পাঠানোর তারিখ</Label>
<DatePicker id="send-on-bn" format="d MMMM yyyy" defaultValue={new Date(2026, 9, 5)} />
</div>
</BooleanUIProvider>
<BooleanUIProvider locale="hi-IN">
<div className="flex flex-col gap-2">
<Label htmlFor="send-on-hi">भेजने की तारीख</Label>
<DatePicker id="send-on-hi" format="d MMMM yyyy" defaultValue={new Date(2026, 9, 5)} />
</div>
</BooleanUIProvider>
</div>
)
}Accessibility
- Semantics
- A native text
input, and abuttonnamed "Choose date" witharia-haspopup="dialog"andaria-expanded. The popup is a non-modaldialognamed "Choose date" holding theCalendargrid (see Calendar), or agridof month or year buttons witharia-selectedon the chosen cell, and, withshowTime,spinbuttons for hours, minutes and AM/PM. Withtrigger="field"the input itself is acomboboxwitharia-haspopup="dialog",aria-expandedandaria-controls(the APG date picker combobox). - Labels
- Name the field with a
Label htmlFororaria-label. The button, the popup and the time spinbuttons are named by the provider stringschooseDate,hour,minuteanddayPeriod; Today and Clear bytodayandclear; the year arrows bypreviousYear,nextYear,previousYearsandnextYears. Giveinlineanaria-label. - Focus
- The button (or Alt+ArrowDown) opens the popup and moves focus to the chosen day, else today, else the 1st. A click in the field with
trigger="field"opens it and leaves focus in the field, so typing goes on. Tab moves through the popup and wraps at its ends. Choosing a day, Escape, Today and Clear close it and return focus to the field; a click outside closes it and leaves focus where the click put it. - Known limits
- The calendar, like the field, follows the provider's
locale(see Calendar); without one, the calendar's day names are react-day-picker's English. - A typed date is read on blur and on Enter, not as each key lands; a screen reader hears the invalid state when focus returns to the field.
showTimeis for single dates in the day view.- The dot of a day template is visual: put its meaning in the day's name with
calendarProps.labels.labelDayButton, as the example does.
- The calendar, like the field, follows the provider's
Keyboard
| Key | Behaviour |
|---|---|
| Alt↓ | In the field: opens the calendar and moves focus to the chosen day, or today. |
| Enter | In the field: reads the typed text now. On the calendar button: opens the calendar. |
| Escape | Closes the calendar and returns focus to the field. |
| Tab | Moves to the next control of the popup; from the last it wraps to the first. |
| → | In the month or year grid: the next month or year (the previous in right-to-left). The day grid's keys are the Calendar's. |
| ↓ | In the month or year grid: the cell below. On an hour, minute or AM/PM spinbutton: one less. |
| ↑ | In the month or year grid: the cell above. On a time spinbutton: one more. |
| Page Down | In the month or year grid: the same cell a year (or a decade) later. |
| Home | In the month or year grid: the first cell of the page. |
| End | In the month or year grid: the last cell of the page. |
API
DatePicker
| Prop | Type | Default | Description |
|---|---|---|---|
calendarProps | DatePickerCalendarProps | Props for the Calendar inside: modifiers and modifiersClassNames for a day template, disabled days, and more. | |
className | string | Classes for the root, which holds the field and its button: set its width here. | |
clearable | boolean | Shows a button that empties the field while it has a value. | |
defaultMonth | Date | The month the calendar opens on when nothing is chosen. Defaults to today's. | |
defaultOpen | boolean | ||
defaultValue | Date | Date[] | null | The date (or dates) it starts with, uncontrolled. | |
fluid | boolean | Fills the width of its container. | |
format | string | A pattern for the field's text, such as "dd/MM/yyyy" or "d MMM yyyy". Defaults to the locale's numeric date. | |
hourCycle | 12 | 24 | 12 (with AM/PM) or 24, for showTime. Defaults to the locale's. | |
icon | ReactNode | Replaces the calendar icon of the button. | |
inline | boolean | Shows the calendar on the page, with no field and no popup. | |
max | Date | The latest date. With showTime, a max that has a time (any but midnight) also bounds the time on its day. | |
min | Date | The earliest date: days before it cannot be chosen, and typing one marks the field invalid. With showTime, a min
that has a time (any but midnight) also bounds the time on its day. | |
mode | "single" | "multiple" | single chooses one date.
multiple chooses several dates; the field lists them with commas (semicolons when a date's text has a comma). | |
numberOfMonths | number | How many months the calendar shows side by side. | |
onOpenChange | ((open: boolean) => void) | ||
onValueChange | ((value: Date | null) => void) | ((value: Date[]) => void) | Called with the new date, or null when the field is emptied or its text is not a date. | |
open | boolean | Whether the calendar is open, when you control it. | |
showButtonBar | boolean | Shows Today and Clear buttons under the calendar. | |
showTime | boolean | Adds hours and minutes under the calendar. Single mode only. | |
size | "default" | "sm" | "lg" | 28, 35 or 42 px tall. Defaults to the provider's controlSize. | |
today | Date | The date marked as today, and the one the Today button chooses. Defaults to the system date. | |
trigger | "button" | "icon" | "field" | What opens the calendar: button, a button joined to the field's end; icon, a calendar icon inside the field;
field, a click on the field itself (and Alt+ArrowDown). | |
value | Date | Date[] | null | The chosen date, when you control it. | |
variant | "default" | "filled" | filled fills the field grey. Defaults to the provider's fieldVariant. | |
view | "month" | "day" | "year" | What is chosen: a day, a month (the 1st of it) or a year (1 January). |
Every part takes className, merged with its defaults by cn(), and ref, which reaches the element it renders.
Data attributes: data-slot="date-picker-panel" (DatePicker), and data-view, data-selected, data-size.
Provider strings: previousYear, previousYears, nextYear, nextYears, chooseYear, hour, minute, dayPeriod, today, clear, chooseDate (BooleanUIProvider's strings).
Theming
The field is Input's look; the joined button is the --secondary fill with the --control edge, darkening to --secondary-hover and --secondary-active. The popup is the popover surface with a 6 px radius and 10 px padding; chosen days, months and years are --primary.
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|---|
--accent | background |
--accent-foreground | text |
--card-foreground | text |
--control | border |
--control-hover | text |
--foreground | text |
--muted-foreground | text |
--primary | background |
--primary-foreground | text |
--ring | outline |
--secondary | background |
--secondary-active | background |
--secondary-foreground | text |
--secondary-hover | background |
--secondary-hover-foreground | text |
--subtle | background |