--- name: date-picker-usage description: > Use after component-usage-ux when an app needs @techsio/ui-kit DatePicker for locale-aware segmented date entry, calendar selection, optional transactional time entry, single or range typed values, and canonical form serialization through one integrated field. metadata: component_version: "1.1.1" type: "core" library: "@techsio/ui-kit" library_version: "0.3.2" requires: "component-usage-ux zag-compound-components app-token-overrides ux-guidelines" sources: "libs/ui/src/molecules/date-picker.tsx libs/ui/src/tokens/components/molecules/_date-picker.css libs/ui/stories/molecules/date-picker.stories.tsx https://zagjs.com/components/react/date-input https://zagjs.com/components/react/date-picker https://react-aria.adobe.com/internationalized/date/" --- # @techsio/ui-kit DatePicker Usage Use DatePicker when a user should type a locale-aware date in segments or pick it from a calendar. Set `granularity` to `hour`, `minute`, or `second` when the same field must also collect a time. Use separate domain controls when date and time have independent lifecycles. ## UX/UI guidelines House rules come from the `ux-guidelines` skill (writing, formatting, states, where actions and feedback live). This section applies them to `DatePicker`. **Use it when** - Entering a specific date or date-time: delivery date, publish schedule, birth date, report period. - When users benefit from both typing (segments) and picking (calendar). **Use something else when** | Need | Use instead | | --- | --- | | Showing a date (read-only) | formatted text via `Intl.DateTimeFormat` | | Relative choices (`Last 7 days`, `This month`) | Select or RadioGroup of presets, optionally with a custom range | | Time-of-day only with an independent lifecycle | a separate time control | **Do** - Pass the app `locale` explicitly — the component defaults to `en-US`, NumericInput to `cs-CZ` (ux-guidelines/formatting#locale-defaults-in-the-kit). - Set `min`/`max` and unavailable dates so impossible choices can't be made, and say why in help text (`Delivery from 2 working days`). - Use `granularity` for date-time instead of two unrelated fields when they form one value. - Show the time zone when it matters (scheduled publishing, cut-offs). **Don't** - Add a hand-written format hint that disagrees with the locale's segment order. - Use a calendar for birth dates far in the past without allowing typing. - Store or display ISO strings to users. **Copy and states** - Label names the date's meaning (`Delivery date`, `Publish at`), not `Date`. - Validation says the allowed range in formatted dates: `Choose a date between 4 Sept 2026 and 30 Sept 2026.` ## Setup Date-only selection uses `CalendarDate | null` and commits a calendar choice immediately: ```tsx import { CalendarDate } from "@internationalized/date" import { DatePicker } from "@techsio/ui-kit/molecules/date-picker" Delivery date ``` Range selection uses a complete readonly tuple. It defaults to two coordinated months and renders distinct start/end segment groups in the same field: ```tsx import { CalendarDate } from "@internationalized/date" import { DatePicker } from "@techsio/ui-kit/molecules/date-picker" Reporting period ``` Timed selection uses `CalendarDateTime | ZonedDateTime | null`. Include the time control and transaction footer: ```tsx import { CalendarDateTime } from "@internationalized/date" import { DatePicker } from "@techsio/ui-kit/molecules/date-picker" Appointment ``` ## Public Contract ```text granularity: day | hour | minute | second selectionMode: single | range size: sm | md | lg value/defaultValue: day -> CalendarDate | null timed -> CalendarDateTime | ZonedDateTime | null day range -> readonly [CalendarDate, CalendarDate] | null timed range -> readonly [CalendarDateTime, CalendarDateTime] | null | readonly [ZonedDateTime, ZonedDateTime] | null onValueChange: ({ value, valueAsString }) => void open/defaultOpen/onOpenChange locale, timeZone, hourCycle, hideTimeZone, shouldForceLeadingZeros min, max, startOfWeek, numOfMonths, isDateUnavailable, isTimeUnavailable disabled, readOnly, invalid, required single: name, form range: startName, endName, form placement, gutter, offset, flip, sameWidth, slide, overflowPadding ``` The `sm`, `md`, and `lg` field sizes follow the shared Input typography and form-control geometry. `readOnly` keeps the normal field surface while removing edit and clear affordances. `invalid` uses the same validation border width and danger-state progression as Input; the normal focus ring remains the focus indicator. ## Core Patterns ### Keep one typed value shape Use `CalendarDate` for a calendar date without time. Use `CalendarDateTime` for a floating wall-clock date-time, or provide a `ZonedDateTime` explicitly when the zone is part of the domain value. Do not pass Zag's internal arrays or a native JavaScript `Date`. Range mode uses only complete two-value tuples. A one-ended range is private input/calendar state and never appears in `value`, `onValueChange`, or form serialization. Timed tuples must contain two floating `CalendarDateTime` values or two `ZonedDateTime` values; never mix the kinds. ```tsx const [value, setValue] = useState(null) setValue(details.value)} > {/* canonical date-only compound parts */} ``` `timeZone` controls presentation and calendar calculations. It does not turn a floating `CalendarDateTime` into a zoned value. Date-only range selection commits after the second valid endpoint. The first endpoint stays private and the popup remains open. `numOfMonths` defaults to two in range mode and one in single mode. ### Treat timed popups as transactions In `hour`, `minute`, and `second` modes, calendar and time edits stay private until Confirm. Cancel, Escape, outside dismissal, and trigger dismissal discard the draft. The main segments continue to show the accepted value and become read-only while the popup is open. Timed range mode follows the same single transaction for both endpoints. `DatePicker.TimeControl` renders separate Start time and End time groups, while one Cancel/Confirm footer accepts or discards the whole interval. Its hour, minute, second, and day-period fields intentionally keep the compact `sm` control density at every DatePicker root size so the calendar remains the primary surface. The first day selected in an empty timed picker is intentionally incomplete. Enter the required time before Confirm becomes available. Do not add app-side callbacks that treat intermediate popup edits as committed values. ### Let a controlled parent remain authoritative `onValueChange` proposes only a committed value. A controlled parent may accept, reject, delay, or transform it; render the `value` prop as the source of truth. If `value` changes externally while a timed draft is open, DatePicker resynchronizes the draft to that accepted value. ### Use the root-owned form value Set `name` in single mode. Set explicit `startName` and `endName` in range mode. The root renders one or two hidden form controls from the accepted value. Serialization is `DateValue.toString()` and never localized display text: ```text CalendarDate -> 2026-08-31 CalendarDateTime -> 2026-08-31T14:30:00 ZonedDateTime -> offset- and zone-preserving string null -> empty string range null -> two empty strings ``` Do not add another hidden input with the same name. `required` and `invalid` communicate field state, but hidden-input native constraint validation is not part of the contract. ### Preserve the accessible compound anatomy Use one `DatePicker.Label`, then Control with Segments and the trigger group. Keep Content inside Positioner. In timed modes, keep TimeControl and the Cancel/Confirm footer inside Content. The high-level Segments, Calendar, and TimeControl parts preserve the private Zag Date Input and Date Picker wiring. Use meaningful label text. The default Clear, Calendar, Cancel, and Confirm trigger labels are accessible; replace their children only with equally clear content or an accessible name. Required editable segments expose `aria-required`. Pass `aria-describedby` to `DatePicker.Segments` when external help or validation text describes the compound field. ## Non-Goals The 1.1 API does not support multiple dates, presets, quick ranges, month-only selection, custom date-cell rendering, raw Zag anatomy, a custom serializer, public partial ranges, or native `Date` values. Do not build those behaviors by reaching through the compound component's internals. ## Common Mistakes ### HIGH Separate custom date and time controls Wrong: ```tsx ``` Correct: ```tsx ``` Source: libs/ui/src/molecules/date-picker.tsx ### HIGH Native Date or incomplete/mixed range Wrong: ```tsx ``` Correct: ```tsx ``` Source: https://react-aria.adobe.com/internationalized/date/ ### HIGH Committing timed popup edits outside Confirm Wrong: ```tsx ``` Correct: ```tsx ``` There is no public draft callback in the 1.1 contract. ### HIGH Duplicate form ownership Wrong: ```tsx {/* parts */} ``` Correct: ```tsx {/* parts */} ``` In range mode, use `startName` and `endName` instead of `name`. ## Validation Commands ```sh rg -n ']*type="(date|datetime-local|time)"|new Date\(' apps rg -n ']*(onDraftValueChange|closeOnSelect|commitMode)' apps rg -n ']*selectionMode="range"[^>]*name=' apps rg -U -P -n ']*name=(?![\s\S]{0,900}