Kanzo UI
Forms

Dates

One machine with a switch — the month grid inline, or the same grid in a popover.

Ark UI

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
        └── CalendarTableNextMonth

The 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.

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

September 1312
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.

September 1312 - October 1312
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
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

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" shows 3/14/2026, 6:30 PM in en-US and 14/3/2026, 18:30 in es-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 the LocaleProvider around 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's DateInput is 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, Backspace clears 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 — invalid on the Field styles the parts, and min and max clamp 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 as parseDate builds a date, and value.toString() reads it back as 2026-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; Esc or 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 read disabled, invalid, readOnly and required. Name the segmented field with aria-label on DatePickerInput: there is no single <input> for a FieldLabel to 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.

3142026630PM

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

KeyDoes
← / →Moves the focused day back / forward one day, crossing into the neighbouring month.
↑ / ↓Moves it back / forward one week.
Home / EndJumps to the first / last day of the visible range.
PageUp / PageDownMoves back / forward one month.
Shift+PageUp / PageDownMoves back / forward one year.
EnterSelects the focused day; in the month or year view it drills down to the next view instead.
EscCloses 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.

PropTypeDefault
granularityDatePickerGranularity — "day" | "minute" | "second""day"

DatePickerTimer

DatePickerTimerProps is an Input's, minus type, with a size.

PropTypeDefault
size"sm" | "md" | "lg""md"
stepnumber — seconds60, or 1 in a "second" picker

DatePickerInput

PropTypeDefault
size"sm" | "md" | "lg""md"
indexnumber0

Renders DatePicker.Control, an InputGroup and the calendar trigger button for you.

CalendarWeekDays

PropTypeDefault
format"narrow" | "short" | "long""narrow"

CalendarTableNextMonth

PropTypeDefault
monthsnumber1

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.

PartWhat it is
CalendarTableHeadThe <thead>.
CalendarTableHeaderOne weekday-name cell inside it.
CalendarTableBodyThe <tbody>.
CalendarTableRowOne week.
CalendarTableCellOne 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.

PartWhat it is
CalendarControlThe row holding the input and the trigger.
CalendarLabelThe field's label.
CalendarTriggerThe button that opens the popover.
CalendarPresetTriggerA button that sets a value — Ark's presets, so value="last7Days" and the rest.
CalendarTodayTriggerAn outline Button reading "Today" that calls selectToday(). It renders its own CalendarContext to reach the machine, so it needs nothing passed to it.
CalendarClearTriggerArk's clear trigger, unstyled — pass a Button with asChild and the label is yours.
CalendarContextThe 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.

On this page