Noctis

Calendar

A composable date grid. Calendar.Root owns the engine — the focused day, the visible month, the selection — and the parts compose freely around it: pagers and a formatted heading over a keyboard-operable month table. The display calendar follows the ambient locale (a Persian locale renders jalali automatically, Persian digits included), values stay immutable CalendarDate objects, and the same surface drops into a page, a popover, a dialog, or a filter panel.

Basic

The canonical composition: a Calendar.Header holding the Previous/Next pagers around the formatted Heading, over the Calendar.Grid month table. Click a day to select it; the value is uncontrolled here. The heading doubles as the zoom control — click it to open the month picker, and again for the year picker; picking a year, then a month, then a day drills back to any date.

Controlled

Control the value with value + onValueChange. The committed value is always emitted in the input value's calendar system (gregorian when there is none) no matter what system is displayed, and value.toString() gives the calendar-native ISO string — ready for a URL or an API. The second onValueChange argument carries the covered period as a { start, end } range for consumers that filter by span.

2026-08-22

Calendar systems

The display system resolves from the ambient locale — switch the docs to Farsi and every calendar on this page renders jalali with no code change. The calendar prop pins a specific Unicode system ("persian", "islamic-umalqura", "hebrew", …) regardless of locale. Weekday order, week start, month names, and digits all come from the locale, so a pinned system still reads naturally in any language.

A switcher is plain composition — the calendar prop is just state:

Bounds and unavailable dates

min/max clamp the selectable window — out-of-range days disable, and the pagers disable when a whole page would fall outside. isDateUnavailable marks individual days unselectable (struck through, skipped on commit) while keeping them focusable, so keyboard users can still traverse them.

Coarse pickers

minView sets the selection floor — the granularity where a pick commits instead of drilling deeper. minView="quarter" turns the same parts into a quarter picker: the grid becomes a scrolling timeline of year sections, the committed value is the period's first day, and the full period arrives as details.range in the onValueChange callback. month, halfyear, and year work the same way; maxView caps how far the heading can zoom out. Every view change cross-fades subtly in the direction of travel — set data-instant on the root to suppress it (reduced-motion users never see it).

Pick a quarter

Granularity tabs

The full period-picker pattern: a segmented strip choosing the granularity, each tab its own picker with that floor. The view model is also fully controllable (view + onViewChange) when a shell needs to own it.

Range selection

RangeCalendar.Root hosts the exact same parts over a { start, end } value. The first pick anchors, the highlight follows the keyboard cursor and the pointer while the second pick is pending, and Escape cancels the anchor. The committed endpoints arrive in the input value's calendar system, and when dates are unavailable a pending selection clamps to the contiguous run around its anchor.

Pick a start, then an end — Escape cancels

Presets

Presets are plain composition on the controlled value — no dedicated part, just buttons that set the range.

Two months, one range

visibleMonths widens the window; compose one Calendar.Grid per month with offset, title each with Calendar.Heading offset, and keep ONE nav cluster — Calendar.Today (jump-to-today) plus the pagers — at the row's end. The band runs seamlessly across the grids, paging moves the whole spread (pageBehavior="single" steps one month instead), and weekdayStyle="twoletter" gives the two-letter weekday labels ("Su Mo Tu" — locales without abbreviations keep their natural narrow forms). The example collapses to a single month below its breakpoint.

Coarse ranges

Under a coarse minView the endpoints snap outward to period boundaries: picking Q1 and then Q3 selects January 1 through September 30. The endpoint pills fill; the span between them washes.

Q1 → Q3 selects the full three quarters

In a dialog

The full filter-dialog recipe: a Dialog shell owning the operator chips, the pill granularity strip, a draft selection, and the Cancel/Apply pair — the calendar itself is unchanged. The day tab is the two-month spread (per-grid titles, one jump-to-today + pager cluster). Each granularity locks its view with minView + maxView, the coarse views scroll instead of paging (so only the day view keeps a header), and the picker sits in a fixed-height region — a blockSize plus the --noctis-calendar-timeline-inline-size / -max-height minted overrides — so switching tabs stretches the period pills to the dialog and never shifts the layout. Apply reads the draft's details.range to describe coarse periods.

In a popover

The classic date-picker shell: the calendar in a Popover, closing on pick. (P2's DatePicker packages this recipe with a segmented input field.)

Week numbers

weekNumbers on Calendar.Grid adds a leading column of locale week ordinals (the CLDR minimal-days rule — ISO 8601 in ISO locales; localized digits) — presentation-only and hidden from screen readers, like the weekday header. The fixed grid widens by exactly one cell square.

Sizes

Three cell scales: sm for dense embedding (a filter panel, a compact popover), md (the default), and lg for standalone surfaces. The pagers ride one control size below the cells.

Custom cells

Calendar.GridBody accepts a function child receiving each day's computed CellData — date, localized label, and every state flag — and typically returns a Calendar.Cell with custom content. Availability dots, prices, activity marks: the cell stays a real day button with its full spoken label.

Headless

Calendar.Root builds its engine from props. When an outer shell needs to own the state — a filter facet, a date-picker popover coordinating a field — build it yourself with useCalendarState(options) — or useRangeCalendarState(options) for a span — and hand it to Calendar.Provider / RangeCalendar.Provider; every part reads the engine through context either way, and the full state surface (focusedDate, visibleRange, selectDate, focusNextPage, …) is yours to drive.

Keyboard

KeyAction
TabMoves focus into the grid, onto the focused day (roving tabindex — one tab stop).
/ Previous / next day. Mirrored under RTL.
/ Same weekday in the previous / next week.
Home / EndFirst / last day of the month.
PageUp / PageDownPrevious / next month (the window pages with the cursor).
Shift + PageUp / PageDownPrevious / next year.
Enter / SpaceSelect the focused day.
EscapeCancels a range selection's pending anchor.

Crossing a month boundary with the arrows pages the calendar and keeps DOM focus on the day that took the cursor. On the coarse views the same map scales to periods: arrows step periods and rows, Home/End jump to the year's first/last period, PageUp/PageDown step a year (a decade with Shift), and the timeline scrolls to keep the cursor visible.

Anatomy

Compose the calendar from its parts. Calendar.Root owns the engine; every other part reads it through context.

  • Calendar.Root — builds the engine from the value/window/system props and names the region ("label, February 2026").
  • Calendar.Provider — the external-engine root: takes the state useCalendarState built.
  • Calendar.Header — the layout row for the pagers and heading, in composition order.
  • Calendar.Heading — the formatted title (month in the day view, year on the timeline) and the zoom-out button while a coarser view exists (override the text via children).
  • Calendar.Previous / Calendar.Next — ghost icon buttons paging the visible window; disabled at the min/max edges.
  • Calendar.Today — the jump-to-today button: moves the cursor (and the window) to today without selecting.
  • RangeCalendar.Root / RangeCalendar.Provider — the range engines; every part above composes inside them unchanged.
  • Calendar.Grid — the month table; owns the keyboard map. Zero-config it renders the header and body.
  • Calendar.GridHead / Calendar.HeadCell — the weekday label row (hidden from screen readers — each day's label carries its weekday).
  • Calendar.GridBody / Calendar.Row — the week rows; GridBody takes the cell-render function.
  • Calendar.Cell — one day: the gridcell band layer wrapping the round day button with the roving tabindex and full spoken label.

On the coarse views Calendar.Grid renders the scrollable year timeline instead of the table — a listbox of year sections (noctis-calendar-timeline, noctis-calendar-year-section, sticky noctis-calendar-year-label) whose period cells reuse the noctis-calendar-cell-trigger slot with their data-view stamped. The rendered year span extends lazily as it scrolls.

Every rendered part carries a data-slot (noctis-calendar on the root, noctis-calendar-cell-trigger on a day button, and so on), and cells stamp their live state — data-selected, data-today, data-unavailable, data-outside-month, data-disabled, data-invalid, plus the range vocabulary data-range-start / data-range-end / data-in-range / data-preview — for host-side styling.

On surfaces

The same calendar re-tuned across the elevation scopes — the root canvas, an elevated panel, a menu, and a sunken well.

root
elevated
menu
sunken

Design tokens

Generated from the component's declaration — the same graph that mints the CSS, so a variable name or its resolution default can't drift. The minted tokens are the public override seam: set one on any ancestor and every calendar in that region retunes — e.g. .sidebar { --noctis-calendar-cell-trigger-size: var(--noctis-size-control-sm); } compacts the grid. Colours aren't minted — cells read the foreground/muted/subtle roles, hover reads the neutral ghost wash, and the selected day reads the accent roles — so a retheme propagates for free. See Customization for the full override ladder and Tokens for the whole graph.

Token

API reference

Generated from the component's types — every prop, type, default, and description comes straight from the source. Each part gets its own table. Expand a row for the full type and description.

Calendar.Root

Prop

Calendar.Provider

Prop

Calendar.Header

Prop

Calendar.Heading

Prop

Calendar.Previous

Prop

Calendar.Next

Prop

Calendar.Today

Prop

Calendar.Grid

Prop

Calendar.GridHead

Prop

Calendar.HeadCell

Prop

Calendar.GridBody

Prop

Calendar.Row

Prop

Calendar.Cell

Prop

AttributeDescription
data-slotThe part anchor every `calendar.css` rule keys off.
data-sizePresent on the root: the cell metrics and type ramp — `sm`, `md`, or `lg`.
data-viewThe grid granularity a cell belongs to — `day`, `month`, `quarter`, `halfyear`, or `year`.
data-interactivePresent on the heading while it is the interactive zoom-out button.
data-cursorPresent on the timeline period cell holding the keyboard cursor.
data-selectedPresent on the selected day's cell and trigger.
data-todayPresent on today's cell and trigger.
data-unavailablePresent on a cell the `isDateUnavailable` callback rejects.
data-outside-monthPresent on leading/trailing cells that belong to an adjacent month.
data-disabledPresent on cells outside `[min, max]`, on a disabled root, and on the disabled pagers.
data-readonlyPresent on the root while the whole calendar is read-only.
data-invalidPresent on the root and the selected cell while the selection violates `[min, max]` or availability.
data-range-startPresent on a range's first cell — where the band's leading corners round.
data-range-endPresent on a range's last cell — where the band's trailing corners round.
data-in-rangePresent on every cell inside the highlighted range (endpoints included) — the band wash.
data-previewPresent on in-range cells while the selection is still pending (anchor set, second pick awaited).
data-week-numbersPresent on the day grid while it renders the opt-in week-number column.
data-drillThe direction of a view change (`in` toward days, `out` toward years) — what the drill animation keys off.
data-instantSet it on the root to suppress the view-change animation (keyboard-driven or test-driven hosts).

RangeCalendar.Root

Prop

RangeCalendar.Provider

Prop