Dates
One machine with a switch — the month grid inline, or the same grid in a popover.
Calendar and DatePicker are one Ark machine with inline flipped: DatePicker is
literally <Calendar inline={false}>. That is why the grid parts are shared, why the props are
identical, and why this is one page — a machine with a switch is a variant, not a second
component.
The grid is a real role="grid" with roving tabindex, arrow-key navigation and month/year
quick-nav, and CalendarTableDays / CalendarWeekDays read the weeks and weekday names off the
machine, so a locale change reflows it with no work on your side.
Inside a Field, DatePicker reads disabled, invalid, readOnly and
required from it, so a disabled field keeps its popover shut as well as its input grey; an explicit
prop on the picker still wins.
Usage
import {
Calendar,
CalendarTable,
CalendarTableDays,
CalendarView,
CalendarViewControl,
CalendarWeekDays,
DatePicker,
DatePickerContent,
DatePickerInput,
} from "@kanzo-tech/ui";Anatomy
Calendar | DatePicker
├── DatePickerInput (the picker only: input + calendar button, or…)
├── DatePickerTrigger (…a button showing DatePickerValue)
└── CalendarView (in DatePickerContent when there is a popover)
├── CalendarViewControl
│ ├── CalendarPrevTrigger
│ ├── CalendarMonthSelect
│ ├── CalendarYearSelect
│ ├── CalendarViewDate
│ └── CalendarNextTrigger
└── CalendarTable
├── CalendarWeekDays
├── CalendarTableDays
└── CalendarTableNextMonthThe month grid
Calendar fixes inline on, so the grid is always visible and there is no trigger or popover.
Reach for it when the calendar is furniture — a booking screen, an availability view — rather
than a form value.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
28 | 29 | 30 | 31 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 1 |
Range
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
28 | 29 | 30 | 31 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 1 |
Two months
CalendarTableNextMonth renders the month months ahead of the visible one, so a two-month
range picker is two tables in one view rather than two calendars.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
28 | 29 | 30 | 31 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 1 |
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
25 | 26 | 27 | 28 | 29 | 30 | 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | 1 | 2 | 3 | 4 | 5 |
With actions
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
28 | 29 | 30 | 31 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 1 |
The popover
DatePicker is the same grid behind an input and a positioned popover — the form shape.
With a trigger button
Range
An ISO string in and out
A form library, a JSON body and a database column all speak YYYY-MM-DD; this machine speaks
Ark's DateValue. The adapter between them is two small functions, and it belongs at your seam
rather than in the library — which side of it your product lives on is a product decision.
Stored as 2026-03-14
A date and a time
A date and a time is one value, so it is one field: set granularity on DatePicker and the
input shows both, in the locale's format, at the height of every other input. The popover is the
calendar with a DatePickerTimer under it, and the timer is the picker's time — setting it sets the
picker's value, and picking a day keeps it.
granularity="minute"shows3/14/2026, 6:30 PMinen-USand14/3/2026, 18:30ines-ES;"second"adds the seconds. It is"day"by default, which is the date-only picker above and changes nothing. The order, the separators and the 12- or 24-hour clock come from theLocaleProvideraround it.- The input is segmented. Day, month, year, hour, minute and, in a 12-hour locale, AM/PM are
separate editable parts — Ark's
DateInput, the machine Shark'sDateInputis made of — because a date and a time cannot be typed as one string into Ark's date input, which admits only digits and the date separator. Click in and type a digit run,14032026, and each part fills and hands on to the next, at typing speed; arrows step a part,Backspaceclears it. The picker reports a value only when every part is filled; clearing any part empties it, and an incomplete entry is never reported. Numbers that are not a date cannot be typed at all, so there is no invalid text to report —invalidon theFieldstyles the parts, andminandmaxclamp what is entered. - Paste takes an ISO timestamp (
2026-03-14T18:30) or one in the locale's order (14/03/2026 18:30,3/14/2026, 6:30 PM) and sets every part at once; anything that is not a date is ignored. A date pasted with no time keeps the value's time, and is midnight without one. - The value is a
CalendarDateTime.parseDateTime("2026-03-14T18:30")builds one, exactly asparseDatebuilds a date, andvalue.toString()reads it back as2026-03-14T18:30:00. A day picked in the calendar while there is no value arrives at midnight — also in an uncontrolled picker, which reports and shows midnight until a time is set — and a time chosen before any day takes the day the calendar is showing. - The popover stays open after a day, so the time can be set;
Escor a click outside closes it. The timer is a native time field: arrows change a segment, digits type one. - Clearing is a
CalendarClearTrigger, wherever you put it in the popover. - Field state. Inside a
Field, the picker and the timer both readdisabled,invalid,readOnlyandrequired. Name the segmented field witharia-labelonDatePickerInput: there is no single<input>for aFieldLabelto point at.
The stored value is one ISO string, and the adapter is the whole of the example: a zone suffix
(Z, +01:00) is not the picker's — it holds local date-times — so the example sets it aside on
the way in and puts it back on the way out. Whether a zone belongs to your value is a product
decision, so the library does not make it for you.
Stored as 2026-03-14T18:30
DatePickerTimer also stands alone, as a plain time field with value and onChange, when a time is
the whole of the value. Its step is in seconds, as on <input type="time">: the default 60 shows
hours and minutes and 1 adds seconds, and inside a picker it follows granularity. Every prop
goes to the <input> itself — aria-label, name, onChange — except size, which is the
group's; the group is w-full, so size it with the element it sits in. There is no DateField: it
was a fixed arrangement of these parts and strictly less capable than them.
Reading a string out is one line, because onValueChange hands you both representations. Going
the other way — hydrating a stored string back into the picker — needs parseDate (or
parseDateTime, above), which @kanzo-tech/ui re-exports along with the DateValue type, for the same reason it re-exports
createListCollection: a machine whose value you cannot build from the barrel is not usable from
the barrel. You do not install @internationalized/date. Wrap the parse in a try/catch: an
unparseable stored value must leave the calendar empty rather than throw it, or one bad row in a
table takes down the whole form.
Keyboard
| Key | Does |
|---|---|
| ← / → | Moves the focused day back / forward one day, crossing into the neighbouring month. |
| ↑ / ↓ | Moves it back / forward one week. |
| Home / End | Jumps to the first / last day of the visible range. |
| PageUp / PageDown | Moves back / forward one month. |
| Shift+PageUp / PageDown | Moves back / forward one year. |
| Enter | Selects the focused day; in the month or year view it drills down to the next view instead. |
| Esc | Closes the popover without changing the value, and returns focus to the trigger that opened it — DatePicker only, since Calendar is inline. |
The grid is a roving tabindex: only the focused cell is reachable with Tab, so Tab leaves the month rather than walking its days.
API Reference
DatePicker
DatePickerProps is Calendar's, plus one prop.
| Prop | Type | Default |
|---|---|---|
granularity | DatePickerGranularity — "day" | "minute" | "second" | "day" |
DatePickerTimer
DatePickerTimerProps is an Input's, minus type, with a size.
| Prop | Type | Default |
|---|---|---|
size | "sm" | "md" | "lg" | "md" |
step | number — seconds | 60, or 1 in a "second" picker |
DatePickerInput
| Prop | Type | Default |
|---|---|---|
size | "sm" | "md" | "lg" | "md" |
index | number | 0 |
Renders DatePicker.Control, an InputGroup and the calendar trigger button for you.
CalendarWeekDays
| Prop | Type | Default |
|---|---|---|
format | "narrow" | "short" | "long" | "narrow" |
CalendarTableNextMonth
| Prop | Type | Default |
|---|---|---|
months | number | 1 |
Both roots extend DatePicker.Root from Ark, so value, defaultValue, onValueChange,
selectionMode, numOfMonths, min, max, locale, startOfWeek, fixedWeeks,
isDateUnavailable, open, disabled and readOnly pass through. lazyMount and
unmountOnExit default to true; DatePicker's positioning defaults to
{ placement: "top" }.
The parts the Anatomy does not show
The tree above is the composition you write. These ship as well, and they are the tier below it: reach for them when you are rearranging the grid rather than using it.
The table, by hand. CalendarWeekDays and CalendarTableDays exist so you never write these —
the first renders the header row, the second the weeks. Both are assembled out of the five below,
and the five are exported so a caller who wants a different cell can build the same table around it.
| Part | What it is |
|---|---|
CalendarTableHead | The <thead>. |
CalendarTableHeader | One weekday-name cell inside it. |
CalendarTableBody | The <tbody>. |
CalendarTableRow | One week. |
CalendarTableCell | One day. Takes the day as value, and its children are the label. |
There is no CalendarTableCellTrigger on our surface and that is deliberate: CalendarTableCell
renders Ark's cell and the trigger inside it, because every state a day carries — selected, in
range, today, unavailable — is painted on the trigger while the rounding of a range's ends is
painted on the cell. Two parts that can never be composed apart are one part.
The picker's own shell. DatePickerInput renders the control and the trigger for you; these are
what it is made of.
| Part | What it is |
|---|---|
CalendarControl | The row holding the input and the trigger. |
CalendarLabel | The field's label. |
CalendarTrigger | The button that opens the popover. |
CalendarPresetTrigger | A button that sets a value — Ark's presets, so value="last7Days" and the rest. |
CalendarTodayTrigger | An outline Button reading "Today" that calls selectToday(). It renders its own CalendarContext to reach the machine, so it needs nothing passed to it. |
CalendarClearTrigger | Ark's clear trigger, unstyled — pass a Button with asChild and the label is yours. |
CalendarContext | The render prop. Takes a function, renders no element of its own, and is how CalendarTableDays reads the weeks. |
DatePicker names two more, and both are worth knowing for what they are rather than what they
sound like. DatePickerPresetTrigger is CalendarPresetTrigger under the picker's own
data-slot — the same shared machine, the same part. And DatePickerTimer is a time field, not
a countdown — see A date and a time.