DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
CalendarDate PickerTime PickerTime RangeTime Slots
AI agentsContributing
  1. Components
  2. Date Picker

Date Picker

A date picker built on Calendar and Base UI Popover — a button trigger or a typed-input anchor (`anchor="input"`), single, range and multiple selection with a full field model (label, validation, clearable), presets, the calendar's markers/booked/caption APIs, a consolidated date-time Apply panel, locale-aware labels and on-color support.

$ pnpm add @tessinaui/ui

When to use

  • A date whose calendar context matters. Day of week, distance from today or availability around it, picked from a compact trigger.
  • Dates with limits or blocked days. Bounds, disabledDates and unavailableDates keep people from choosing a day that cannot work.
  • Typed entry in forms with anchor="input": an invoice or start date people already know and would rather type. The calendar stays one Tab away. Single dates only; ranges keep the button.
  • A range, several dates, or a date and time from one trigger: mode="range" with presets, mode="multiple", or the date-time Apply panel.
  • A label that carries the format. Put format expectations in supportingText or the label, never placeholder-only: a placeholder disappears the moment a value exists. The input anchor puts the locale's format there by default.

When not to use

  • For a calendar that stays on the page. Use Calendar: the same grid inline, no popover. Everything DatePicker forwards (markers, booked days, selection variants, the date-time panel) also works there directly.
  • For a memorable, known date such as a date of birth. Typing a date you already know beats navigating to it, so use a plain Field. If you keep a picker for it, use the captionLayout="dropdown" recipe (see Date of birth) rather than making someone page back through hundreds of months.
  • For a time alone. Use TimePicker; the built-in date-time panel covers date and time together.
  • To choose among a handful of known dates, such as delivery slots. Use Select or Combobox rather than any-date entry.
  • When people only have a relative time like "now" or "tonight". Offer it as an option in a Select, or as a preset.

Installation

Terminal
pnpm add @tessinaui/ui react-day-picker

Usage

example.tsx
import { DatePicker, createRangePresets } from "@tessinaui/ui";
example.tsx
{/* Single date with the field model */}
<DatePicker
  mode="single"
  label="Departure"
  required
  clearable
  selected={date}
  onSelect={setDate}
  errorMessage={error}
/>

{/* Date range with presets sidebar */}
<DatePicker
  mode="range"
  selected={range}
  onSelect={setRange}
  presets={createRangePresets()}
  numberOfMonths={2}
/>

{/* Typed input — type the date, or open the calendar button */}
<DatePicker
  mode="single"
  anchor="input"
  label="Departure"
  selected={date}
  onSelect={setDate}
/>

Examples

Default

A single date picker with a calendar icon trigger.

Typed input

anchor="input" turns the field into a text input. Type the date in the locale's order — slashes, dashes, dots or spaces all work, and 2026-06-24 does too — and it's read when you press Enter or leave the field; nothing is masked while you type. The calendar button beside it is its own tab stop and the only way to open the popup. The format sits in the helper row until you set supportingText. Text that can't be read, or a day outside the bounds, shows an error and leaves the value alone — try 24.06.2026 in the first field, then 12/31/2025.

Labels and helper text

The input-family field model: visible label (top or start), optional marker, supporting text — all wired into the trigger's accessible name and description.

Validation

required marks the trigger and the calendar (the selection can't be emptied); errorMessage replaces the supporting text, implies the error intent and is announced politely. intent="warning" paints the boundary without the error semantics.

States

disabled, readOnly (focusable, popup stays closed) and isLoading (read-only semantics + aria-busy + spinner in place of the calendar icon).

Clearable

An in-trigger × clears the value, returns focus to the trigger, and answers Escape on the closed trigger. Follows Field's clear-button conventions, tab stop included.

Range

Range selection with two months side by side. A fresh range closes the popover after the end date; adjusting one endpoint of an existing range keeps it open for further browsing.

Range limits

min / max bound the number of nights; excludeDisabled resets a range that would swallow a disabled day.

Multiple

Multiple selection stays open across picks and summarises the trigger ("3 dates selected"); formatValue overrides the summary.

Presets

Preset shortcuts in a left sidebar (or a top strip). Picking one commits and closes — unless an Apply footer is present, in which case presets only set the selection (preview-style) and Apply commits.

Date & time

The calendar's own Apply panel (one implementation for Calendar and DatePicker): start/end date and time fields, optional timezone select, localized labels. Apply reports a CalendarApplyData and closes the popover.

Date of birth

captionLayout="dropdown" + bounds turns paging into month/year menus — the far-past navigation recipe.

Markers and legend

The calendar's markers (intent dots that extend day accessible names), showLegend, and the auto-hiding Today button — all through the popover.

Booked and blocked days

unavailableDates renders reachable-but-taken days (struck through, focusable, announced, unselectable); disabledDates removes days from reach entirely.

Selection variants

The calendar's selection inks: outline (habit-tracker ring) and line (hotel-style underline range).

On-color

tone="on-color" (set directly or inherited from a Surface) swaps the trigger onto the shared ink-well recipe; the popover itself stays a standard floating surface, per the family rule.

Locale and RTL

locale drives the trigger label (via Intl.DateTimeFormat, range labels via formatRange), the calendar, and the panel; dir="rtl" defaults the locale to Arabic. When dir is unset nothing is stamped — the component no longer forces ltr.

A multiple selection's summary follows the locale's plural rules: in Arabic, two dates, three to ten, and eleven or more each take their own form.

Sizes

Calendar cell density across sm, md, and lg — trigger, chips, panel fields and popup all follow. Trigger heights land on 32/40/48px with a mouse; on a touch screen the 44px touch floor raises sm and md to 44px while lg keeps its 48px, so the smaller two deliberately look alike there — open one to see the cell density differ.

Loading

DatePickerSkeleton for content that isn't there yet; isLoading for a known value that's busy.

API Reference

Selection

PropTypeDefaultDescription
mode"single" | "range" | "multiple""single"Selection mode; selected/onSelect types follow it
selectedDate | DateRange | Date[]—Controlled selection value
onSelect(value) => void—Called when selection changes (react-day-picker's extra args pass through)
formatValue(selected) => string—Override the trigger label for the current selection
shouldCloseOnSelectbooleanmode-awareDefault: single closes on pick, range closes when a fresh pick completes, multiple stays open; always suppressed while showDateTime/footer provide a commit step

Field

PropTypeDefaultDescription
labelReactNode—Visible label, associated with the trigger; the trigger's accessible name reads label + current value
labelPlacement"top" | "start""top"Label position
labelWidth"sm" | "md" | "lg" | "xl" | "auto" | CSS length"md"Width of the start-label column, so sibling fields in a form share one control column — sm 6rem · md 8rem · lg 11rem · xl 14rem. Labels wrap, never truncate. Only applies with labelPlacement="start", from sm up
visuallyHiddenLabelbooleanfalseKeep the label for AT only
optionalbooleanfalse"(optional)" marker (mutually exclusive with required)
supportingTextReactNode—Helper line under the trigger, in aria-describedby
errorMessageReactNode—REPLACES supportingText, implies intent="error", announced politely
intent"none" | "error" | "warning" | "success" | "info""none"Boundary/ring color on the outline trigger
requiredbooleanfalseStar + sr-only "(required)" in the label name (button anchor) or aria-required on the input (input anchor), + react-day-picker required (selection can't be emptied)
disabledbooleanfalseDisables the trigger. Day matchers here are deprecated — see disabledDates
readOnlybooleanfalseFocusable, popup stays closed, intent paint suppressed (readOnly ⊃ disabled ⊃ error)
isLoadingbooleanfalseRead-only semantics + aria-busy + spinner
clearablebooleanfalse× button (with a value); Escape on the closed trigger also clears
onClear() => void—After a clear
tone"default" | "on-color"contextInk-well recipe on colored surfaces; inherited from Surface

Trigger

PropTypeDefaultDescription
anchor"button" | "input""button"input: a text field you type the date into, with a calendar button as the only popup entry. Single mode only — range and multiple keep the button (dev warning)
placeholderstring"Pick a date" · noneButton anchor: the label shown with nothing selected. Input anchor: the input's placeholder, empty by default — the format lives in the helper row, never placeholder-only
parseInput(text: string) => Date | nullbuilt-inInput anchor: replaces the lenient locale-order parser; return null to reject. Bounds and disabled days are still enforced after it
triggerVariantButtonProps["variant"] | "filled""outline"Button anchor: outline reads as a field well, filled as the tinted well with no outline until focus or an intent; others are plain buttons
triggerSizeButtonTextSizefollows sizeButton anchor: trigger size override
triggerClassNamestring—Extra classes for the trigger — the field shell under the input anchor (e.g. "w-56")

How the input anchor reads a date. Fields go in the locale's order (MM/DD/YYYY en-US, DD.MM.YYYY de-DE, YYYY/MM/DD ja-JP) — a month/day swap is never guessed. A four-digit first field is read as year-month-day in any locale. Two-digit years pivot at 30 (26 → 2026, 30 → 1930); three-digit years are rejected as typos. The field is always Gregorian, and it reads the locale's own digits and punctuation, so it can always read back what it shows.

Calendar options

PropTypeDefaultDescription
size"sm" | "md" | "lg""md"Calendar cell scale; trigger/chips/fields follow (32/40/48px triggers)
numberOfMonthsnumber1Months in the popover (collapses to 1 below 640px)
showWeekNumber · showOutsideDays · fixedWeeksboolean—Display passthroughs
fromDate / toDateDate—Day-accurate selectable bounds
disabledDatesMatcher | Matcher[]—Days that cannot be selected (react-day-picker matchers)
unavailableDatesMatcher | Matcher[]—Reachable-but-taken days (struck through, focusable, guarded)
defaultMonthDatetodayOpening month when nothing is selected; a selection always wins
localestring"en-US" ("ar" at dir="rtl")BCP-47 tag for trigger label, calendar and panel
stringsPartial<DatePickerStrings>English, Arabic under dir="rtl"Override any UI string (calendar strings + dialogLabel*, clearLabel, datesSelected, the optionalLabel marker, and the input anchor's invalidDateError — {format} interpolates — and unavailableDateError). datesSelected takes plural variants for the locale, datesSelected_<category> (datesSelected_few), picked by Intl.PluralRules; overriding datesSelected drops the Arabic table's variants
captionLayout"label" | "dropdown" | "dropdown-months" | "dropdown-years""label"Month/year menus; reverseYears flips the year order
markersmatcher or { match, intent, label }[]—Intent dots + accessible-name suffix; showLegend builds the legend from labelled entries
renderDayExtra(date, modifiers) => ReactNode—Second line inside day cells
selectionVariant"fill" | "outline" | "line""fill"Selection ink style
showTodayButtonbooleanfalseFooter Today button (auto-hides out of bounds)
weekStartsOn0–6localeFirst day of week
excludeDisabledbooleanfalseRange resets when it would include a disabled day
min / maxnumber—Range: nights between endpoints · multiple: pick counts
calendarPropsPartial<CalendarProps>—Escape hatch, spread last onto the Calendar (wins)

Presets

PropTypeDefaultDescription
presetsPresetItem[]—{ label, getValue } shortcuts; active state tracks the selection in every mode
presetsPosition"left" | "top""left"Sidebar or chip strip

Date & time panel

PropTypeDefaultDescription
showDateTimebooleanfalseRender the calendar's Apply panel (start/end date + time)
showTimebooleantrueInclude the time fields
showTimezonebooleanfalseTimezone select in the panel
timezone / onTimezoneChangestring / (zone) => void—Controlled IANA zone
dateTimeLayout"vertical" | "horizontal""vertical"Panel beside or below the grid
onApply(data: CalendarApplyData) => void—Apply payload (start/startTime/end/endTime/dates/timezone); Apply also closes the popover
footerReactNode—Custom footer slot (when showDateTime is off); its presence also suppresses close-on-select

Values are plain local Dates — the timezone select reports a choice in the Apply payload and never converts the dates — timezone math belongs to the consumer, which alone knows what the date means.

Popover

PropTypeDefaultDescription
rounded"none" | "sm" | "md" | "lg" | "xl" | "full""lg"Popup + calendar + trigger cascade; full caps the panel at 24px per the panel rule
align / sidepositioning"center" ("start" under the input anchor) / "bottom"Popover placement; the input anchor's popup hangs from the whole field
open / defaultOpen / onOpenChangecontrolled open—reason carries Base UI's reasons plus "select", "preset-press", "apply-press"
dir"ltr" | "rtl"inheritedStamped only when set. Unset, the picker follows the page, keyboard included
classNamestring—Wrapper classes; other root props split aria-* → trigger, rest → wrapper

Keyboard

KeyWhereAction
Enter / SpacetriggerOpen; focus moves to the selected day (else today)
EscapepopupClose, focus returns to the trigger
Escapeclosed triggerClear (only with clearable and a value; otherwise not consumed)
Enterinput (input anchor)Read the typed date; text that can't be read stays, with an error
TabinputAlso reads the typed date, then moves to the clear button (if shown) and the calendar button. Focusing the input never opens the popup
EscapeinputRevert an edit; on an unedited clearable field, clear. Not consumed otherwise
Enter / Spacecalendar buttonOpen; on close focus returns to the button
Arrowsday gridMove by day/week
Shift+←/→ · Shift+↑/↓day gridPrevious/next month · year (react-day-picker)
PageUp / PageDown (+Shift)day gridPrevious/next month (year) keeping the day
Home / Endday gridStart/end of week
Enter / SpacedaySelect; single mode then closes and returns focus
TabpopupThrough nav, dropdowns, presets, panel fields — the popover is not a focus trap

Accessibility

  • Pattern. A button trigger (aria-haspopup="dialog") opening a named non-modal dialog (role="dialog" from Base UI Popover, aria-label per mode — "Choose date" / "Choose date range" / "Choose dates", localized). The APG date-picker-dialog pattern is followed for focus and keyboard, with a non-modal popover stance instead of aria-modal — no modality is claimed that isn't enforced.
  • Input anchor pattern. anchor="input" renders a plain text input (no combobox role — it owns no listbox) and a separate calendar button that is its own tab stop and the only popup entry, the APG date-picker-dialog shape. Unlike Combobox's trigger, which leaves the tab order because ArrowDown opens its list, this button has to stay reachable: a text input has no other keyboard path to the calendar. On close, focus returns to the button.
  • Initial focus lands on the selected day, else today (react-day-picker autoFocus), not the first nav button.
  • Names. With a label, the button trigger's accessible name is label + current value ("Departure Jun 24, 2026"), so the value is announced from the collapsed state; data-empty marks the placeholder state. The input anchor is named by its <label> and announces its own value; its calendar button reads "Choose date, Jun 24, 2026" once a value is set. Helper/error text rides aria-describedby; the error swap happens inside one polite region.
  • One live region. A wrapper-level role="status" announces the committed selection (localized, formatRange-deduplicated). The calendar's own caption region announces month changes. No nested regions.
  • On the button anchor, aria-invalid and aria-required are deliberately absent — neither is a supported attribute on role="button" (axe critical); the error state rides the described-by message, the intent boundary and data-invalid, and required rides the label name. The input anchor stamps both natively: they are legal on a real input.
  • Touch targets: on any touch screen, tablets included, the trigger is ≥44px (Button floor) and the clear button carries a 44px tap area on both anchors; the input anchor's field floors at 44px and its text at 16px, so iOS does not zoom on focus (the Button type ladder only applies to the button anchor); day cells and panel controls follow the calendar's floors. On a 320px screen the popup keeps 1px from each edge so its 44px grid fits whole.
  • AT matrix: VoiceOver + Safari passed 2026-09-14. The trigger reads "Jun 24, 2026 dialog pop up button". Space opens "Choose date web dialog", and ArrowRight lands on "Thursday, June 25th, 2026 button" inside "June 2026 table 7 columns, 5 rows". Enter closes it and the status region reads "Jun 25, 2026". NVDA and JAWS: UNTESTED on this machine.
  • Known limitation. readOnly/isLoading triggers expose aria-disabled (a read-only button has no native state); the value stays in the name.

Agent notes

Stable hooks: data-slot="date-picker" (wrapper, data-mode, data-anchor="button" | "input"), date-picker-input-shell and date-picker-input (input anchor — its calendar button keeps date-picker-trigger), date-picker-trigger (data-empty/data-invalid/data-readonly/data-loading), date-picker-value, date-picker-clear, date-picker-popup, date-picker-presets, date-picker-label, date-picker-supporting, date-picker-announcer, date-picker-footer, plus every calendar-* slot inside the popup. The trigger also carries Base UI's data-popup-open while open.

Migration

OldNewNotes
disabled={matchers}disabledDates={matchers}Bridged for one minor with a dev warning; disabled is now the boolean trigger state
DateTimeApplyDataCalendarApplyDataSame shape; the alias remains for one minor. The payload gains dates[] in multiple mode
rounded="full" popup 36px24px (rounded-3xl)The panel-radius rule; xl (also 24px on the popup, 12px on the trigger) is new
Popup stayed open after single selectCloses + returns focusshouldCloseOnSelect={false} restores the old behavior
dir defaulted "ltr"unset by defaultSet dir explicitly to stamp it
Trigger label always en-USlocalized via locale—
—anchor="input"New and additive; the button stays the default

PresetItem / DateRange

example.ts
import { createSinglePresets, createRangePresets } from "@tessinaui/ui";
import type { DateRange, PresetItem } from "@tessinaui/ui";

interface PresetItem {
  label: string;
  getValue: () => Date | DateRange;
}
CalendarTime Picker

On this page

When to useWhen not to useInstallationUsageExamplesDefaultTyped inputLabels and helper textValidationStatesClearableRangeRange limitsMultiplePresetsDate & timeDate of birthMarkers and legendBooked and blocked daysSelection variantsOn-colorLocale and RTLSizesLoadingAPI ReferenceSelectionFieldTriggerCalendar optionsPresetsDate & time panelPopoverKeyboardAccessibilityAgent notesMigrationPresetItem / DateRange