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.
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
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, 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
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: an InputGroup around <input type="time" step="1"> with a clock addon, for the
half of a timestamp a calendar cannot express.