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'23<CalendarPreview>4 <CalendarPreview.Trigger>5 <CalendarPreview.Input />6 </CalendarPreview.Trigger>7 <CalendarPreview.Content>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.
The table below flattens the root props for reading. The exported
CalendarPreviewProps is a discriminated union of CalendarPreviewSingleProps,
CalendarPreviewRangeProps and CalendarPreviewMultipleProps: selection
narrows value, defaultValue and onValueChange to a single shape, and
lock exists only on the range arm. Type a wrapper against one of those arms,
or against CalendarPreviewBaseProps for the props that do not vary by
selection — Omit over the union collapses it and loses the discriminant.
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
Nav
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
Footer
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>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<CalendarPreview2 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<CalendarPreview2 commit="explicit"3 defaultMonth={new Date(2024, 3, 1)}4 defaultOpen5>6 <CalendarPreview.Trigger>7 <CalendarPreview.Input />8 </CalendarPreview.Trigger>9 <CalendarPreview.Content>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.Preset4 range={{ from: new Date(2024, 3, 11), to: new Date(2024, 3, 17) }}5 >6 Last 7 days7 </CalendarPreview.Preset>8 <CalendarPreview.Preset9 range={{ from: new Date(2024, 2, 19), to: new Date(2024, 3, 17) }}10 >11 Last 30 days12 </CalendarPreview.Preset>13 <CalendarPreview.Preset14 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>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.
Triggerrenders a non-button element. With a plain trigger — a label, a calendar icon — Base UI supplies the button semantics:role,tabindex,aria-haspopup,aria-expanded, andaria-disabledrather than adisabledattribute.- When the trigger contains
InputorRangeInputinstead, it drops that button role and its tab stop, because in ARIA a button's children are presentational and the field would not be announced as editable. Tab reaches the field directly, andArrowDownin it opens the calendar. Contentthen declines initial focus on its own, so keystrokes reach the field rather than the grid. PassinitialFocusexplicitly only to override that.Escapein a typed field is two-stage, as in a combobox: the first press reverts the text being edited, the second dismisses the popover.- The
Navcaption is anaria-live="polite"region, so changing month is announced. MonthGridcells are buttons witharia-pressed, not tabs — the design reuses the standalone tab visual, but tab semantics without tabpanels would be wrong.readOnlyleaves days legible and focusable while refusing edits;disabledremoves them from interaction and prevents the popover opening at all.loadingimpliesdisabled, so nothing is operable over data that has not arrived. Each shimmer region carriesaria-busy, and the shimmer itself isaria-hidden.