Appearance
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, 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:
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,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 |
granularity | "day" | "hour" | "minute" | "second" | Smallest editable unit; time granularities add time segments |
hourCycle | 12 | 24 | Force 12- or 24-hour time; defaults to the locale |
autoFocus | boolean | Focus the first segment on mount |
locale | string | BCP-47 locale override |
showDayOfWeek | boolean | Show the locale's short day of the week next to the date |
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, hourCycle, firstDayOfWeek, timeZone, locale, showDayOfWeek, 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).