Askr documentation
UI & Componentsexperimental

Calendar and Date Picker

Calendar and Date Picker: anatomy, keyboard behavior, state, and theming in Askr.

Example

import { DatePicker, DatePickerInput } from '@askrjs/themes/components';

<DatePicker value={dueDate()} onValueChange={setDueDate}>
  <DatePickerInput aria-label="Project due date" />
</DatePicker>
Try Calendar

Browse a calendar surface for date selection.

Purpose

Calendar renders the visual grid of a month view and Date Picker renders a date input — both are presentation-only wrappers from @askrjs/themes, with no @askrjs/ui headless calendar or date-picker primitive backing them. There's no month math, no range-selection logic, and no popover wiring built in; you get markup and ARIA roles for a date grid, and you supply the state.

Install and import

Import Calendar, CalendarHeader, CalendarCaption, CalendarNav, CalendarPreviousButton, CalendarNextButton, CalendarGrid, CalendarHead, CalendarBody, CalendarRow, CalendarCell, and CalendarDay from @askrjs/themes/calendar. DatePicker and DatePickerInput come from @askrjs/themes/date-picker. Both subpaths compile to the same shared components barrel as the rest of the catalog, so treat the subpath as documentation of intent rather than a separate bundle.

Live examples

A working calendar means computing the days for the visible month yourself — leading and trailing days from adjacent months, which cells are today, selected, or part of a range — and passing the right booleans to CalendarDay on every render. Date Picker's baseline example is simpler: DatePickerInput defaults to type="date", so a plain native date input with no extra JavaScript is a legitimate example on its own.

Anatomy

The calendar grid nests CalendarGrid (role="grid") containing CalendarHead (role="row") for weekday labels and CalendarBody holding CalendarRow (role="row") elements of CalendarCell (role="gridcell") wrapping CalendarDay buttons. CalendarHeader holds CalendarCaption, the month/year label, and CalendarNav with CalendarPreviousButton and CalendarNextButton. DatePicker is just an outer slot around DatePickerInput, which renders a real <input>.

State model

None of it is tracked internally. CalendarDay takes disabled, outside, rangeStart, rangeMiddle, rangeEnd, selected, and today as plain booleans that it turns into matching aria-* and data-* attributes — your code decides which day gets which flag on every render, including recomputing the whole grid when the visible month changes. DatePickerInput has no controlled-value helpers beyond what a native <input> already gives you; read and write its value the way you would any form input.

Keyboard and accessibility

CalendarGrid's role="grid" structure is correct, but grid-style arrow-key navigation between days isn't wired up — you'd need to manage focus movement and tabindex yourself to match the roving-tabindex pattern the ARIA grid role expects. CalendarPreviousButton and CalendarNextButton default to aria-label="Previous month"/"Next month" (overridable via children), so navigation controls are at least labeled for screen readers out of the box. DatePickerInput inherits full native <input> keyboard support for free since it's a real <input type="date"> by default.

Styling and tokens

Every calendar part carries a data-slot (calendar-day, calendar-nav, calendar-grid, and so on), and CalendarDay additionally exposes data-selected, data-today, data-outside, data-range-start, data-range-middle, and data-range-end so you can style day states from CSS without reaching into JavaScript. The nav buttons pick up the shared btn btn-icon btn-ghost classes, matching icon buttons elsewhere in the catalog. DatePickerInput applies the same input class as Input and other form fields.

API

Everything except CalendarDay accepts only the generic CatalogComponentProps shape (as, asChild, class, children, ref) with no component-specific typed props. CalendarDay adds the seven typed booleans listed above. DatePickerInput accepts a type prop, defaulting to "date", plus whatever native <input> attributes you spread in — there's no DatePickerProps type covering things like min, max, or a selected-range shape, because that logic doesn't exist here.

Edge cases

Multi-date and range selection are entirely your responsibility — the rangeStart/rangeMiddle/rangeEnd flags exist so you have somewhere to put the result, not because the component computes ranges for you. If you need a popover-attached date picker rather than a bare input, you'll have to pair DatePicker with Popover yourself; there's no built-in trigger/content relationship between the two.

See Popover if you're building a calendar-in-a-popover date picker, since Date Picker doesn't include that wiring. See Native Select and Input OTP for a comparison of experimental components where the native element does more of the accessibility work for you.