kenos UIBeta
Primitive

Date Picker

Headless date primitives for React — segmented input, popover calendar, range and multiple selection. Built on native Intl, timescape, and Floating UI. Zero CSS shipped.

Features

  • Single, range, and multiple selection via one Root API (mode).
  • Segmented date input powered by timescape — locale-aware order and separators.
  • Popover calendar with Floating UI positioning, or inline calendar via composition.
  • Full keyboard navigation across segments, day/month/year grids — RTL-aware arrows.
  • WAI-ARIA grid and spinbutton semantics built in.
  • Unavailable dates (focusable, not selectable) vs disabled dates.
  • HiddenInput for native HTML form submission.
  • Form props: name, required, invalid, errorMessage.
  • Unstyled — style with className, style, and data-* attributes.
  • Localization via Intl (locale, weekStartsOn, dir) — no bundled locale data.
  • Customizable screen-reader messages via messages prop.
  • Time segments via granularity (hour, minute, second) — powered by timescape.
  • DatePicker.Presets + useDatePickerActions for shortcut buttons.
  • pageBehavior for multi-month grids; allowsNonContiguousRanges for booking ranges.
  • Fully controlled mode="multiple" with value sync.

Installation

Install the Date Picker primitive on its own, or use the aggregator @kenos-ui/react package if you already depend on other Kenos primitives.

Install a package

pnpm add @kenos-ui/react-datepicker

Import

import { DatePicker } from "@kenos-ui/react-datepicker";

Examples

Every demo is interactive and keyboard-navigable.

Default

The README pattern — controlled state with useState, segmented Input, Trigger, and the Calendar shorthand. Toggle the code tabs to see unstyled, CSS, Tailwind, and Panda CSS approaches.

Styling works with plain className on each part, then data-* selectors from your Content scope for calendar cells. Keep display: table-cell on td[role=gridcell] — do not use flex/grid on table cells.

Composition

For full control, compose ViewControl, Grid, and Day with a render prop instead of the Calendar shorthand.

Composables

Use only the parts you need — input-only, calendar-only, or the full popover path in Default.

A segmented input without a calendar — omit Content and Trigger. Ideal for birth dates and compact forms.

Controlled

Pass value and onValueChange on DatePicker.Root for fully controlled state.

Selected:

Date Range

Set mode="range" with dual inputs (index={0} and index={1}). Live hover preview styles days with data-in-range. Press Escape after picking a start date to cancel the pending anchor without closing the popover.

Select a start date, then press Escape to cancel the pending range without closing.

Multiple Dates

mode="multiple" toggles dates on click. Pass value and onValueChange for fully controlled sync — programmatic updates and form resets stay in step with the calendar. Keep the popover open with closeOnSelect={false}.

No dates selected yet

Localization

Segment order, separators, weekday names, and week start derive from Intl. Pass any BCP 47 locale tag.

Selected date: Sunday, July 12, 2026
SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311

Min and Max

Clamp the selectable range with minDate and maxDate on Root. Disabled dates show data-disabled.

SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311

Disabled Dates

Pass disabled={(date) => boolean} to disable specific dates — weekends in this example. Disabled cells receive no focus and use aria-disabled.

SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311

Unavailable Dates

Use unavailable when dates should stay focusable for keyboard users but must not be selected — e.g. booked slots. Cells expose data-unavailable and rich aria-label text.

SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311

Form Native

Add DatePicker.HiddenInput with name on Root for native <form> submission. Pair with required for browser validation.

Invalid State

Set invalid and errorMessage on Root to wire aria-invalid and aria-errormessage on the input group. Style with data-invalid on Root.

Please select a valid date

RTL

Pass dir="rtl" (or rely on document.dir) to mirror arrow-key navigation in day, month, and year grids.

Presets

Wrap shortcut buttons in DatePicker.Presets inside Content. Use useDatePickerActions() for selectDate, selectRange, selectToday, and related helpers — no manual dispatch wiring.

Time Granularity

Set granularity to "hour", "minute", or "second" to add time segments via timescape. Use hourCycle={12 | 24} to force 12h or 24h display. Calendar selection updates the date portion and preserves the current time.

Selected date: Monday, June 15, 2026

Jun 15, 2026, 14:30

Non-contiguous Ranges

By default, completing a range fails when unavailable dates sit between start and end. Set allowsNonContiguousRanges on mode="range" to allow booking-style ranges that skip unavailable days (weekends, blocked slots).

Weekends are unavailable but you can still book Mon → Fri with allowsNonContiguousRanges.

Select a range

Page Behavior

pageBehavior="visible" (default) changes the visible month on PageUp/PageDown. pageBehavior="single" moves focus by one month without changing the month header while the target day is still rendered in the grid — useful for future multi-month layouts.

<DatePicker.Root pageBehavior="single">
  <DatePicker.Content><DatePicker.Calendar /></DatePicker.Content>
</DatePicker.Root>

Year Navigation

In year view, PrevTrigger / NextTrigger page through 12-year windows. minDate / maxDate disable out-of-range years.

SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311

Form Integration

Bind value and onValueChange to your form library. All examples validate with Zod.

Styling

Target data-* on Root and day cells. Root exposes data-open, data-invalid, data-focus-within, and more for field-level states.

Accessibility

Kenos implements the WAI-ARIA date picker and grid patterns. Roles, labels, and focus management are handled for you. Disabled dates are removed from the tab order; unavailable dates remain focusable with descriptive labels.

Roles and state

PartRole / attributes
DatePicker.Inputrole="combobox" (popover path)
DatePicker.Triggeraria-haspopup, aria-expanded, aria-controls
DatePicker.Contentrole="dialog"
DatePicker.Gridrole="grid"
DatePicker.Dayrole="gridcell" · aria-selected · aria-current="date"
Input segmentsrole="spinbutton" · aria-valuenow · aria-valuetext

Keyboard support

Behavior depends on which part has focus — trigger, segments, or calendar grid. Try the inline calendar below with arrow keys.

Trigger

KeyDescription
Enter / SpaceOpen popover and move focus into the calendar.
Arrow ↓Open popover when closed.
EscapeClose popover when open; restore focus to trigger.

Segments (Input)

KeyDescription
0–9Type digits; auto-advance when segment is full.
Arrow ↑ / ↓Increment or decrement focused segment.
Arrow ← / → / TabMove between month, day, year segments.
Alt + Arrow ↓Open popover when closed.
Alt + Arrow ↑Close popover when open.
BackspaceClear focused segment.
EscapeClose popover when open.

Day grid

KeyDescription
Arrow keysMove focus by day (roving tabindex).
PageUp / PageDownPrevious / next month (pageBehavior visible). single: move focus ±1 month within grid when the target day is visible. Shift + Page = year.
Home / EndFirst / last day of focused week.
Enter / SpaceSelect focused date.
Tab / Shift+TabMove between the day grid and header controls (Prev, month/year label, Next).
EscapeClose popover. In range mode with pending start, cancels anchor instead.

Header controls

KeyDescription
Tab / Shift+TabMove between Prev, ViewTrigger, Next, and the active grid.
Enter / SpaceActivate Prev/Next (change month or year page) or ViewTrigger (switch day → month → year view).

Month / Year grids

KeyDescription
Arrow keysMove between months or years.
PageUp / PageDownYear view: previous / next 12-year page.
Enter / SpaceSelect month or year; drill into next view.
EscapeReturn to previous view or close popover.

API Reference

Props, data attributes, and keyboard interactions. Import from @kenos-ui/react-datepicker.

Root props

mode"single" | "range" | "multiple"default: "single"

Selection mode. Controls value shape and Input index usage.

value / defaultValue / onValueChangeDate | null · DateRange · Date[]

Controlled or uncontrolled value. Shape depends on mode.

open / defaultOpen / onOpenChangeboolean

Popover open state (popover path only).

localestringdefault: navigator.language

BCP 47 tag — segment order, separators, labels, week start.

weekStartsOn0 – 6

Override first day of week (0 = Sunday).

minDate / maxDateDate

Clamp selectable range.

disabledboolean | (date: Date) => boolean

Disable dates — no focus, no selection (aria-disabled on cells).

unavailable(date: Date) => boolean

Mark dates unavailable — focusable but not selectable (data-unavailable).

readOnlyboolean

Prevent edits via segments or grid.

dir"ltr" | "rtl"

Text direction for keyboard navigation. Defaults to document.dir.

messagesPartial<DatePickerMessages>

Override prev/next/view labels and range screen-reader prompts.

namestring

Form field name for HiddenInput.

requiredboolean

Native required on HiddenInput.

invalidboolean

Sets aria-invalid on the input group.

errorMessagestring

Linked via aria-errormessage when invalid.

placeholderDateDate

Calendar opens focused on this date.

granularity"day" | "hour" | "minute" | "second"default: "day"

Lowest time unit in Input segments. day = date only; minute adds hour + minute.

hourCycle12 | 24

12h vs 24h time segments. Defaults from locale when omitted.

pageBehavior"visible" | "single"default: "visible"

Day grid PageUp/Down: visible changes month view; single moves focus within the grid when possible.

allowsNonContiguousRangesbooleandefault: false

Range mode only. Allow completing a range that spans unavailable dates between start and end.

onFocusWithin / onBlurWithin / onFocusChangeFocusEvent handlers

Focus-within callbacks on Root (popover excluded).

closeOnSelectboolean | CloseOnSelectConfigdefault: true (single)

Close popover after selection. Granular per mode when object.

modalbooleandefault: false

Opt-in focus trap + aria-modal on Content.

Presets component

childrenReactNode

Shortcut buttons or links. Pair with useDatePickerActions for selectDate, selectRange, etc.

className / style

Layout wrapper for preset controls.

useDatePickerActions

selectDate(date)

Dispatch SELECT_DATE — single or toggle in multiple.

selectRange(start, end)

Dispatch SET_RANGE.

selectDates(dates)

Dispatch SET_SELECTED_DATES — multiple mode.

selectToday()

Select today's date.

selectDaysFromToday(n)

Select today + n days.

modeSelectionMode

Current Root mode from context.

HiddenInput

namestring

Override Root name. Renders nothing if no name is set.

Input props

index0 | 1

Range mode: 0 = start segment, 1 = end segment.

segmentLabels{ month?, day?, year? }

Accessible labels for spinbutton segments.

Content props

portalboolean

Render into document.body.

side / align / sideOffset / alignOffset

Floating UI positioning.

forceMountboolean

Keep in DOM when closed (exit animations).

avoidCollisionsbooleandefault: true

Flip when no space.

Data attributes (Root)

AttributeOn partDescription
[data-open]Popover is open.
[data-invalid]invalid prop is true.
[data-focus-within]Focus is inside the field (excludes popover).
[data-readonly]readOnly prop is true.
[data-disabled]disabled prop is true.
[data-required]required prop is true.

Data attributes (Day)

AttributeOn partDescription
[data-selected]Selected day.
[data-today]Current date.
[data-outside-month]Adjacent month day.
[data-disabled]Disabled (min/max or disabled fn) — no focus.
[data-unavailable]Unavailable callback — focusable, not selectable.
[data-in-range]Inside range selection.
[data-range-start] / [data-range-end]Range endpoints.