Skip to content
View as Markdown

DatePicker ​

Three related components for date input — a segmented field, a field with a calendar popover, and a standalone calendar grid. Built on @internationalized/date and Base UI.

Import ​

tsx
import {
  DateField,
  DatePicker,
  DateRangePicker,
  Calendar,
  RangeCalendar,
  Field,
  // Date value helpers (re-exported from @internationalized/date)
  parseDate,
  getLocalTimeZone,
  today,
  type CalendarDate,
  type DateValue,
  type DateRange,
} from "@tailor-platform/app-shell";

API shape ​

DateField and DatePicker are standalone composite controls.

  • They own date entry, keyboard behavior, constraints, locale/timezone handling, and form value serialization.
  • They expose standard labeling hooks: id, aria-label, aria-labelledby, aria-describedby, and isInvalid.
  • They also auto-wire into Field.Root, so Field.Label, Field.Description, Field.Error, and form validation state work the same way as the other AppShell form controls.

DateField ​

Standalone usage with an accessible name:

tsx
<DateField aria-label="Invoice date" />

With a visible label + description:

tsx
<label id="invoice-date-label" htmlFor="invoice-date">
  Invoice date
</label>
<DateField
  id="invoice-date"
  aria-labelledby="invoice-date-label"
  aria-describedby="invoice-date-help"
/>
<p id="invoice-date-help">Format follows your locale</p>

Inside Field.Root:

tsx
<Field.Root name="invoiceDate">
  <Field.Label>Invoice date</Field.Label>
  <DateField />
  <Field.Description>Format follows your locale</Field.Description>
</Field.Root>

Controlled:

tsx
const [date, setDate] = useState<CalendarDate | null>(null);

<DateField aria-label="Invoice date" value={date} onChange={setDate} />;

DatePicker ​

A DateField with a calendar popover.

tsx
<DatePicker aria-label="Ship date" />

Constrained + unavailable dates:

tsx
<label id="delivery-date-label" htmlFor="delivery-date">
  Delivery date
</label>
<DatePicker
  id="delivery-date"
  aria-labelledby="delivery-date-label"
  minValue={today(getLocalTimeZone())}
  isDateUnavailable={(date) => {
    const dow = date.toDate(getLocalTimeZone()).getDay();
    return dow === 0 || dow === 6; // weekends
  }}
/>

Week start:

tsx
<DatePicker aria-label="Date" firstDayOfWeek="mon" />

Validation and errors ​

Use standard HTML + ARIA when rendering the field standalone:

tsx
<label id="delivery-date-label" htmlFor="delivery-date">
  Delivery date
</label>
<DatePicker
  id="delivery-date"
  aria-labelledby="delivery-date-label"
  aria-describedby={error ? "delivery-date-error" : undefined}
  isInvalid={!!error}
  value={value}
  onChange={setValue}
/>
{error && <p id="delivery-date-error">{error}</p>}

Or let Field.Root wire the label, description, and error elements:

tsx
<Field.Root name="deliveryDate" error={error ? { message: error } : undefined}>
  <Field.Label>Delivery date</Field.Label>
  <DatePicker value={value} onChange={setValue} />
  <Field.Error match={!!error}>{error}</Field.Error>
</Field.Root>

Calendar ​

A standalone calendar grid for custom date-selection UIs.

tsx
<Calendar aria-label="Select date" onChange={(date) => console.log(date)} />

DateRangePicker ​

Start and end segmented inputs sharing one calendar popover, with a { start, end } value (DateRange) — a separate component from DatePicker, not a mode. Same composition model as the other controls: standalone with aria-*, or inside Field.Root. Selection follows the react-aria model — the first calendar pick anchors the range and keeps the popover open, the highlight follows the pointer/arrows, and the second pick completes it; picking backwards swaps the endpoints, while a range typed in reverse is flagged invalid (Field.Error match="customError") rather than swapped.

tsx
const [range, setRange] = useState<DateRange | null>(null);

// standalone
<DateRangePicker aria-label="Billing period" value={range} onChange={setRange} />

// composed
<Field.Root name="period">
  <Field.Label>Billing period</Field.Label>
  <DateRangePicker />
  <Field.Error match="customError" />
</Field.Root>

A single combined proxy input is registered as the one Field control — its value is empty until both ends are complete, so isRequired blocks a partial range. Give that combined input a name for a single start/end native-POST field (a wrapping Field.Root name wins), or use startName / endName to emit two plain hidden inputs for classic form-POST.

RangeCalendar ​

The standalone inline range calendar (the grid inside DateRangePicker), for custom layouts.

tsx
<RangeCalendar aria-label="Stay dates" onChange={(range) => console.log(range)} />

Localization ​

Locale and timezone come from AppShell automatically. Override per field with locale / timeZone:

tsx
<DatePicker aria-label="Date" locale="ja-JP" />

Day of the week ​

showDayOfWeek adds the locale's short day of the week where the locale places it — 2025/12/19(金) for ja-JP, Fri, 12/19/2025 for en-US. It is read-only and derived from the entered date: it shows a placeholder until the date is complete, and while a date segment is mid-entry it keeps the previous day of the week (muted), so typing 25 into the day never flashes the 2nd's. Screen readers hear the full day name with each date segment's value.

tsx
<DatePicker aria-label="Closing date" showDayOfWeek />

Keyboard ​

  • Segments: ↑/↓ increment/decrement, digits type-to-fill (auto-advance), ←/→ move between segments, Backspace clears, / commits the current segment and advances.
  • Whole-date shortcuts: t today · m/h start/end of the entered month · y/r start/end of the year · w/k start/end of the week · - previous day · =/+ next day.
  • Calendar grid: arrows move by day/week, Home/End to week start/end, PageUp/PageDown by month, Shift+PageUp/PageDown by year, Enter/Space selects. Alt+↓ opens the calendar from the field.

Props ​

DateFieldProps ​

PropTypeDescription
value / defaultValueDateValue | nullControlled / uncontrolled value
onChange(v: DateValue | null) => voidFires when the value changes
onBlur() => voidFires when focus leaves the whole segmented control
minValue / maxValueDateValueInclusive date range bounds
isDateUnavailable(date: DateValue) => booleanMarks specific dates unavailable
isDisabledbooleanDisables interaction and form submission
isReadOnlybooleanAllows focus/navigation without editing
isRequiredbooleanMarks the control required
isInvalidbooleanAdds invalid styling / aria-invalid to the segmented UI
placeholderValueDateValueSeeds unset segments
granularity"day" | "hour" | "minute" | "second"Smallest editable unit; time granularities add time segments
hourCycle12 | 24Force 12- or 24-hour time; defaults to the locale
autoFocusbooleanFocus the first segment on mount
localestringBCP-47 locale override
showDayOfWeekbooleanShow the locale's short day of the week next to the date
namestringEmits a form value through the proxy input
idstringProxy input id (use with external <label htmlFor>).
firstDayOfWeek"sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat"Override the locale week start used by w / k shortcuts
aria-label / aria-labelledbystringAccessible name
aria-describedbystringIDs of description / error elements
classNamestringRoot element class

DatePickerProps ​

All DateFieldProps, plus:

PropTypeDescription
timeZonestringIANA timezone for resolving "today"
firstDayOfWeek"sun" | "mon" | ...Force the calendar's first column

CalendarProps ​

See the calendar docs in-code: controlled/uncontrolled value, min/max, unavailable dates, focused date, locale, timezone, accessible naming, and className.

DateRangePickerProps ​

The DatePickerProps surface (labeling, isInvalid, min/maxValue, isDateUnavailable, granularity, hourCycle, firstDayOfWeek, timeZone, locale, showDayOfWeek, autoFocus, isDisabled/ReadOnly/Required), with the range-specific differences:

PropTypeDescription
value / defaultValueDateRange | nullControlled / uncontrolled { start, end } range
onChange(v: DateRange | null) => voidFires with a complete range, or null when cleared/incomplete
namestringOne combined hidden input (start/end) for native POST; Field.Root name wins
startName / endNamestringEmit two plain hidden inputs with the ISO start/end (classic POST)

A typed range with end before start sets the invalid state and reports a built-in customError message (Field.Error match="customError"); the calendar always commits ordered endpoints.

RangeCalendarProps ​

Same surface as CalendarProps, with value / defaultValue: DateRange | null and onChange: (v: DateRange) => void (fired once per selection, when the second date completes the range).