CalendarPreview

One subcomposed date component that owns date state and popover state explicitly.
1<CalendarPreview defaultMonth={new Date(2024, 3, 1)}>
2 <CalendarPreview.Nav />
3 <CalendarPreview.Grid />
4</CalendarPreview>

CalendarPreview replaces Calendar, DatePicker and RangePicker with a single root and dot-notation parts. Every piece of state is owned explicitly — selection, visible month, open, granularity — so nothing is private and no part needs to reach around another.

It ships alongside the current calendar family; those exports are removed a release after this one is documented.

Anatomy

1import { CalendarPreview } from '@raystack/apsara'
2
3<CalendarPreview>
4 <CalendarPreview.Trigger>
5 <CalendarPreview.Input />
6 </CalendarPreview.Trigger>
7 <CalendarPreview.Content initialFocus={false}>
8 <CalendarPreview.GranularityTabs />
9 <CalendarPreview.Nav />
10 <CalendarPreview.Grid />
11 <CalendarPreview.MonthGrid />
12 <CalendarPreview.Footer>
13 <CalendarPreview.Cancel />
14 <CalendarPreview.Apply />
15 </CalendarPreview.Footer>
16 </CalendarPreview.Content>
17</CalendarPreview>

Drop any part you do not need. Grid renders for the day granularity and MonthGrid for the rest, so a picker offering both keeps both in the tree.

API Reference

Root

Owns every piece of state and provides it to the parts.

Prop

Type

Trigger

Anchors the popover. Renders a div, never a <button>, because it may contain a typed input.

Prop

Type

Content

The portaled surface. Positioning props are passed here directly.

Prop

Type

Input

The typed single-date field.

Prop

Type

RangeInput

Paired start and end fields. Both are typable.

Prop

Type

Caption, a revert-to-default button, and previous / next. Renders for the day granularity only — the other granularities scroll rather than page.

The revert button appears only when the root was given a defaultValue and the current value differs from it; pressing it restores that default. It is absent otherwise rather than disabled, because a control that can never do anything is noise.

Prop

Type

Grid

The day grid.

Prop

Type

MonthGrid

Month, quarter, half-year and year selection, as a scrolling list of years.

Prop

Type

GranularityTabs

Day, Month, Quarter, Half-year and Year. Renders only when the root offers more than one granularity.

Prop

Type

Presets

Holds Preset buttons, as a column beside the grid or a row above it.

Prop

Type

Preset

One preset. It writes straight into root state, so it needs no callback of its own, and marks itself pressed while the current value matches.

It deliberately does not close the popover: under commit="explicit" that would discard the very edit it just made, and for a range you want to see what was applied. Compose Apply, or close from onValueChange.

Passing range to a single picker — or value to a range one — fails at render rather than on click, so the stack points at the preset.

Prop

Type

Action row for Apply and Cancel.

Prop

Type

Examples

State

Open, visible month, and bounds are all ordinary props. The visible month is independent of the value but initialises from it, so a picker holding a date in another year opens on that year rather than today.

1<CalendarPreview defaultOpen onOpenChange={(open) => console.log(open)}>
2 <CalendarPreview.Trigger>
3 <CalendarPreview.Input />
4 </CalendarPreview.Trigger>
5 <CalendarPreview.Content initialFocus={false}>
6 <CalendarPreview.Nav />
7 <CalendarPreview.Grid />
8 </CalendarPreview.Content>
9</CalendarPreview>

Granularity

granularities lists what the user may switch between; the tabs appear only when there is more than one.

1<CalendarPreview
2 defaultMonth={new Date(2024, 3, 1)}
3 granularities={["day", "month", "quarter", "half-year", "year"]}
4>
5 <CalendarPreview.GranularityTabs />
6 <CalendarPreview.Nav />
7 <CalendarPreview.Grid />
8 <CalendarPreview.MonthGrid />
9</CalendarPreview>

MonthGrid emits the first day of the chosen period — a quarter pick in 2024 Q3 yields 1 July 2024 — and onValueChange's second argument names the granularity that produced it. The pair is what makes the value unambiguous: a Date alone cannot distinguish 1 June picked as a day from June picked as a month.

The typed field follows the active granularity too, reading Jun 2026, Q3 2026, H1 2026 or 2026 rather than a full date.

It also reads across granularities: typing Q4 2027 into a day field switches to Quarter and commits in one go, and a bare Q4 resolves against the year on screen. The active granularity is always tried first, and only granularities the picker actually offers are considered — so a day-only picker rejects Q4 rather than switching to a tab that is not there.

Commit and locking

commit="explicit" buffers edits until Apply, so a popover can be abandoned without the parent seeing intermediate states. lock holds one endpoint of a range read-only while the other stays pickable.

1<CalendarPreview
2 commit="explicit"
3 defaultMonth={new Date(2024, 3, 1)}
4 defaultOpen
5>
6 <CalendarPreview.Trigger>
7 <CalendarPreview.Input />
8 </CalendarPreview.Trigger>
9 <CalendarPreview.Content initialFocus={false}>
10 <CalendarPreview.Nav />
11 <CalendarPreview.Grid />
12 <CalendarPreview.Footer>
13 <CalendarPreview.Cancel />
14 <CalendarPreview.Apply />
15 </CalendarPreview.Footer>

Presets

Presets write straight into root state and light up while they match.

1<CalendarPreview selection="range" defaultMonth={new Date(2024, 3, 1)}>
2 <CalendarPreview.Presets>
3 <CalendarPreview.Preset
4 range={{ from: new Date(2024, 3, 11), to: new Date(2024, 3, 17) }}
5 >
6 Last 7 days
7 </CalendarPreview.Preset>
8 <CalendarPreview.Preset
9 range={{ from: new Date(2024, 2, 19), to: new Date(2024, 3, 17) }}
10 >
11 Last 30 days
12 </CalendarPreview.Preset>
13 <CalendarPreview.Preset
14 range={{ from: new Date(2024, 3, 1), to: new Date(2024, 3, 30) }}
15 >

Loading

loading replaces the caption and the grid with a shimmer and disables every control. It is one flag rather than two: the old family shimmered five grid rows while the chrome stayed live, so the month controls were still operable over data that had not arrived.

1<CalendarPreview loading defaultMonth={new Date(2024, 3, 1)}>
2 <CalendarPreview.Nav />
3 <CalendarPreview.Grid />
4</CalendarPreview>

Inside a Field

Input reads field context, so the label association, required and aria-invalid all wire up by composition. The component renders no error text itself — report through onValidityChange and let Field.Error present it.

1<Field>
2 <Field.Label>Starts</Field.Label>
3 <CalendarPreview>
4 <CalendarPreview.Trigger>
5 <CalendarPreview.Input />
6 </CalendarPreview.Trigger>
7 <CalendarPreview.Content initialFocus={false}>
8 <CalendarPreview.Nav />
9 <CalendarPreview.Grid />
10 </CalendarPreview.Content>
11 </CalendarPreview>
12 <Field.Error />
13</Field>

Slots

Every rendered element carries a data-slot. The names are public API covered by semver, so styling may target them and a rename is a breaking change.

Slot
calendar-preview-apply
calendar-preview-cancel
calendar-preview-content
calendar-preview-day
calendar-preview-day-number
calendar-preview-footer
calendar-preview-granularity
calendar-preview-grid
calendar-preview-input
calendar-preview-meridiem
calendar-preview-month-cell
calendar-preview-month-grid
calendar-preview-month-grid-year
calendar-preview-nav
calendar-preview-nav-caption
calendar-preview-nav-next
calendar-preview-nav-previous
calendar-preview-nav-undo
calendar-preview-positioner
calendar-preview-presets
calendar-preview-range-inputs
calendar-preview-skeleton
calendar-preview-table
calendar-preview-time-field
calendar-preview-trigger
calendar-preview-weeks

Accessibility

  • The day grid is react-day-picker's, which supplies the grid roles, roving tabindex and arrow-key navigation.
  • Trigger renders a non-button element with button semantics supplied by Base UI: it carries role, tabindex, aria-haspopup, aria-expanded, and aria-disabled rather than a disabled attribute.
  • Pass initialFocus={false} to Content whenever the trigger contains a typed field. Without it the popup takes focus on open and keystrokes never reach the field.
  • The Nav caption is an aria-live="polite" region, so changing month is announced.
  • MonthGrid cells are buttons with aria-pressed, not tabs — the design reuses the standalone tab visual, but tab semantics without tabpanels would be wrong.
  • readOnly leaves days legible and focusable while refusing edits; disabled removes them from interaction and prevents the popover opening at all.
  • loading implies disabled, so nothing is operable over data that has not arrived. Each shimmer region carries aria-busy, and the shimmer itself is aria-hidden.