DatePicker
import { DatePicker } from '@tangible/ui';
Interactive docs and examples: DatePicker in Storybook.
DatePicker props
| Prop | Type | Default | Description |
|---|---|---|---|
aria-describedby? |
string |
ID of element(s) that describe the trigger. Field.Control injects this so helper text and errors are announced; without it the injection is silently dropped (no rest-spread on this component). |
|
aria-invalid? |
"false" | "true" | boolean |
Marks the control invalid. Lands on the trigger button, which also wears tui-input-group — that is what lights the danger border. |
|
aria-label? |
string |
||
aria-labelledby? |
string |
ID of the element that labels the trigger. Field.Control injects this, and it outranks aria-label per AccName 2A — which is why the trigger's own aria-label fallback does not have to change. The trigger's value span is appended so the current date stays audible — except under asChild, where there is no value span and a phantom id would strip the name. |
|
calendarLabels? |
CalendarLabels |
||
children? |
ReactNode |
Trigger placement. Omit and the picker renders its own input-group button, exactly as before. Provide children to place the trigger yourself — inside a text field's suffix slot, a toolbar — with <DatePicker.Trigger> marking the spot, and asChild on that Trigger to supply your own element: <DatePicker value={date} onChange={commit}> <TextInput value={text} onChange={(e) => setText(e.target.value)} suffix={ <DatePicker.Trigger asChild> <IconButton icon="lms/calendar" label="Pick a date" /> </DatePicker.Trigger> } /> </DatePicker> Children with no Trigger among them leaves the picker unopenable; a dev warning fires for that case. |
|
className? |
string |
||
defaultValue? |
Date | null |
||
disabled? |
boolean |
||
hour12? |
boolean |
true |
12-hour time UI with an AM/PM toggle. |
id? |
string |
||
isDayDisabled? |
(day: Date) => boolean |
Per-day disable hook, applied on top of min/max. | |
labels? |
DatePickerLabels |
||
locale? |
string |
the document language, falling back to 'en-US' |
BCP 47 locale for month/weekday names and numeric formats. |
max? |
Date |
Latest selectable day (inclusive, time-of-day ignored). | |
min? |
Date |
Earliest selectable day (inclusive, time-of-day ignored). | |
onChange? |
(value: Date) => void |
||
value? |
Date | null |
||
weekStartsOn? |
0 | 1 | 2 | 3 | 4 | 5 | 6 |
1 |
First day of the week. 0 = Sunday … 6 = Saturday. |
withTime? |
boolean |
false |
Include the time field (and format the trigger with time). |
zoneLabel? |
ReactNode |
Display-only zone badge next to the time field, e.g. "UTC+0". |
DatePicker.Trigger
Also exported as DatePickerTrigger.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild? |
boolean |
Render children as the trigger instead of the built-in input-group button. The child owns its accessible name and receives no chrome — see this file's header for what that costs and why. |
|
children? |
React.ReactNode |
Trigger element, used when asChild is set. |
|
className? |
string |
Other exports
defaultDatePickerLabels:Required<DatePickerLabels>
Examples
-
With Field Error: DatePicker inside Field with Field.Error.
Field.Controlinjectsaria-invalidandaria-describedbyonto the trigger, which is also the element wearingtui-input-group— so the shared primitive lights the danger border and AT announces the error text.aria-requiredis deliberately NOT forwarded: the trigger isaria-haspopup="dialog"on a plain button, androle="button"does not support that attribute. Field.Label carries the requirement instead. -
asChild trigger:
DatePicker.Trigger asChildswaps the built-in input-group button for whatever you hand it — the escape hatch for cases where the affordance is an icon button beside free text, not a date field. The popover (time field, Now, month/day/year, calendar) is unchanged.<DatePicker />with no children keeps rendering its own trigger, so this is additive: reach forDatePicker.Triggeronly when you need to place the trigger yourself.Three things differ from the default trigger, all deliberate:
- The child owns its accessible name, so name it for the action ("Pick a date") rather than the value — unlike the default trigger, the selected date is not appended to the name.
- No
tui-input-groupchrome is applied (that is the point), soaria-invalidforwards without painting anything. A custom trigger inside aFieldwith an error needs its own[aria-invalid="true"]rule — see the AsChildInvalid story below. disabledarrives asaria-disabled+data-disabled, since the child may not be a<button>— see AsChildDisabled below.
The child must be natively focusable and keyboard-activatable (a
<button>or<a>, not a styled<div>): underasChildthe picker attaches no key handler of its own. Nothing else changes —onChangestill hands back aDate, and the child's ownonClickstill runs.The play function opens the popover from the custom trigger: the visual suite cannot see portaled content, so this is what proves the cloned ref and props wiring survives in a real browser.
-
asChild trigger (disabled): A disabled picker cannot hand the child a native
disabled— the child may not be a<button>. It getsaria-disabled+data-disabledinstead, and the picker refuses to open, which is what the play function asserts by keyboard — the element stays focusable and Enter reaches the handler, where the guard turns it away. (A mouse click proves less: IconButton's ownaria-disabledrule setspointer-events: none, so the click never arrives at all.)Only the open direction is guarded. Blocking close as well would strand an open popover when a form disables its controls mid-submit.
-
asChild trigger (invalid, in a Field):
Field.Controlinjectsaria-invalidandaria-describedbyonto the custom trigger exactly as it does the built-in one, andField.Labelreaches the accessible name througharia-labelledby. What does not come along is the danger border: it belongs totui-input-group, whichasChilddeliberately does not apply.So an invalid custom trigger needs one rule of your own — the attribute is already on the element, it just isn't painted:
.my-date-trigger[aria-invalid='true'] { border-color: var(--tui-theme-danger-base); color: var(--tui-theme-danger-base); } -
Composed into a text field: The composition the compound shape exists for: a text field the user can type into freely, with the picker hanging off its suffix slot. Typed text like "3pm ET" survives — no picker produces a timezone label — and the calendar button fills in what a picker is good at.
TUI never writes into the field.
onChangehands back aDateand the consumer decides what string to insert, which is what makes this work with a format TUI knows nothing about.The input group's own click handler focuses the inner input, but bails on anything inside a
button(TextInput.tsx'shandleGroupClick), so the trigger keeps its click and the caret stays put.
Other stories: Default, With time (Pick a Date and Time), Disabled, Min/Max bounds (interactive page).
Component API tokens
Read via fallback, never declared. Set one on an ancestor to restyle every instance under it.
--tui-calendar-cell-size--tui-calendar-cell-radius--tui-calendar-selected-bg--tui-calendar-selected-fg--tui-calendar-range-bg--tui-calendar-today-bg--tui-calendar-caption-color--tui-date-picker-width