DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

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.

Playground

Installation

pnpm add @tessinaui/ui react-day-picker

Usage

import { DatePicker, createRangePresets } from "@tessinaui/ui";
{/* 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}
/>

When to use

  • DatePicker — picking a date whose calendar context matters (day of week, distance from today, availability around it), from a compact trigger.
  • Calendar — the same grid inline on the page, no popover. Everything DatePicker forwards (markers, booked days, selection variants, the date-time panel) also works there directly.
  • Plain Fields — memorable, known dates (date of birth on a form): GOV.UK's research is blunt that a calendar hurts there. If you keep a picker for it, use the captionLayout="dropdown" recipe below rather than making someone page back 400 months.
  • TimePicker — time alone; the built-in date-time panel covers date + time together.
  • Select / Combobox — choosing among a handful of known dates (delivery slots) rather than any date.
  • anchor="input" — forms where people already know the date and would rather type it (an invoice date, a start date). The calendar stays one Tab away. Single dates only; ranges keep the button.

Format expectations belong in supportingText or the label — never placeholder-only (it disappears the moment a value exists). The input anchor puts the locale's format there by default.

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.

Sizes

Calendar cell density across sm, md, and lg — trigger, chips, panel fields and popup all follow. Trigger heights land on 32/40/48px on desktop; on phones the §5 touch floor raises sm and md to 44px while lg keeps its 48px, so the smaller two deliberately look alike below md — 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"]"outline"Button anchor: outline reads as a field well; 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>—Override any UI string (calendar strings + dialogLabel*, clearLabel, datesSelected, and the input anchor's invalidDateError — {format} interpolates — and unavailableDateError)
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 (the Mantine/Astryx stance; timezone math belongs to the consumer).

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"—Stamped and fed to Base UI only when set; unset leaves the document direction alone
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 React Aria's 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: trigger ≥44px on phones (Button floor); 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 TES-23 floors.
  • AT matrix: VoiceOver verified locally. NVDA and JAWS: UNTESTED on this machine — status stays beta until a pass is on record.
  • 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

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

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

On this page

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