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
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, andisInvalid. - They also auto-wire into
Field.Root, soField.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:
<DateField aria-label="Invoice date" />With a visible label + description:
<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:
<Field.Root name="invoiceDate">
<Field.Label>Invoice date</Field.Label>
<DateField />
<Field.Description>Format follows your locale</Field.Description>
</Field.Root>Controlled:
const [date, setDate] = useState<CalendarDate | null>(null);
<DateField aria-label="Invoice date" value={date} onChange={setDate} />;DatePicker
A DateField with a calendar popover.
<DatePicker aria-label="Ship date" />Constrained + unavailable dates:
<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:
<DatePicker aria-label="Date" firstDayOfWeek="mon" />Validation and errors
Use standard HTML + ARIA when rendering the field standalone:
<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:
<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.
<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.
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.
<RangeCalendar aria-label="Stay dates" onChange={(range) => console.log(range)} />Localization
Locale and timezone come from AppShell automatically. Override per field with locale / timeZone:
<DatePicker aria-label="Date" locale="ja-JP" />Keyboard
- Segments:
↑/↓increment/decrement, digits type-to-fill (auto-advance),←/→move between segments,Backspaceclears,/commits the current segment and advances. - Whole-date shortcuts:
ttoday ·m/hstart/end of the entered month ·y/rstart/end of the year ·w/kstart/end of the week ·-previous day ·=/+next day. - Calendar grid: arrows move by day/week,
Home/Endto week start/end,PageUp/PageDownby month,Shift+PageUp/PageDownby year,Enter/Spaceselects.Alt+↓opens the calendar from the field.
Props
DateFieldProps
| Prop | Type | Description |
|---|---|---|
value / defaultValue | DateValue | null | Controlled / uncontrolled value |
onChange | (v: DateValue | null) => void | Fires when the value changes |
onBlur | () => void | Fires when focus leaves the whole segmented control |
minValue / maxValue | DateValue | Inclusive date range bounds |
isDateUnavailable | (date: DateValue) => boolean | Marks specific dates unavailable |
isDisabled | boolean | Disables interaction and form submission |
isReadOnly | boolean | Allows focus/navigation without editing |
isRequired | boolean | Marks the control required |
isInvalid | boolean | Adds invalid styling / aria-invalid to the segmented UI |
placeholderValue | DateValue | Seeds unset segments |
autoFocus | boolean | Focus the first segment on mount |
locale | string | BCP-47 locale override |
name | string | Emits a form value through the proxy input |
id | string | Proxy 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-labelledby | string | Accessible name |
aria-describedby | string | IDs of description / error elements |
className | string | Root element class |
DatePickerProps
All DateFieldProps, plus:
| Prop | Type | Description |
|---|---|---|
timeZone | string | IANA 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:
| Prop | Type | Description |
|---|---|---|
value / defaultValue | DateRange | null | Controlled / uncontrolled { start, end } range |
onChange | (v: DateRange | null) => void | Fires with a complete range, or null when cleared/incomplete |
name | string | One combined hidden input (start/end) for native POST; Field.Root name wins |
startName / endName | string | Emit 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).