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.

Live preview in the UI Catalogue →

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" />

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
autoFocusbooleanFocus the first segment on mount
localestringBCP-47 locale override
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, firstDayOfWeek, timeZone, locale, 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).