Calendar Preview
A subcomposed calendar that owns its selection and view state.1<CalendarPreview defaultMonth={new Date(2024, 3, 1)}>2 <CalendarPreview.Days />3</CalendarPreview>
Anatomy
Every part renders its own default, so composition is opt-in depth:
1import { CalendarPreview } from '@raystack/apsara'23<CalendarPreview>4 <CalendarPreview.Days />5</CalendarPreview>
Expanded, the day view is a header and a grid:
1<CalendarPreview>2 <CalendarPreview.Days>3 <CalendarPreview.Header>4 <CalendarPreview.Caption />5 <CalendarPreview.Reset />6 <CalendarPreview.PrevMonth />7 <CalendarPreview.NextMonth />8 </CalendarPreview.Header>9 <CalendarPreview.Grid>10 <CalendarPreview.Day />11 <CalendarPreview.Weekday />12 </CalendarPreview.Grid>13 </CalendarPreview.Days>14 <CalendarPreview.Footer />15</CalendarPreview>
Children override the content a part computes from context, so
<CalendarPreview.Caption>Q3 2024</CalendarPreview.Caption> replaces the month label.
API Reference
CalendarPreview
The root. Owns the selected value and the visible month, provides both to every part, and renders a column that hugs its content. Also takes render, className and ref.
Prop
Type
CalendarPreview.Days
The day view — a header and a grid. Hugs its content rather than reserving a fixed height.
Prop
Type
CalendarPreview.Caption
The month label above the grid, and optionally the trigger for the month and year scroller.
Prop
Type
CalendarPreview.Grid
The day grid. Layout and per-day data live here rather than on the root, so a calendar with two grids can configure them independently.
Prop
Type
CalendarPreview.Header
The row above the grid. Composes .Caption, .Reset, .PrevMonth and .NextMonth when given no children. Takes render, className and ref.
CalendarPreview.PrevMonth / CalendarPreview.NextMonth
Step the view one month. Never disabled by minDate or maxDate — bounds limit selection, not navigation.
CalendarPreview.Reset
Restores defaultDate. Renders only when there is something to restore.
CalendarPreview.Trigger
Anchors the popover and owns opening it. Renders the formatted value, or the placeholder, when given no children — wrap an .Input in it for a typeable field. Never renders a button, so the control inside stays focusable. Takes render, className and ref.
CalendarPreview.Content
The portaled popover surface. Takes Popover.Content props — side, align, sideOffset and the rest — and flips above the trigger on collision.
CalendarPreview.Input
Prop
Type
CalendarPreview.Body
The popup body: label, input, scale switcher and the view for the active scale. Renders all four when given no children. Takes render, className and ref.
CalendarPreview.Scales / CalendarPreview.Scale
The scale switcher, built on Apsara Tabs. Renders nothing when only one scale is offered, so a plain day calendar never grows a one-tab row. .Scale is only needed to relabel or reorder.
CalendarPreview.Panel
The view container. Mounts all five views; each gates on the active scale itself, so .Quarters can be mounted alone with no day grid in the tree.
CalendarPreview.Months / .Quarters / .HalfYears / .Years
Year-grouped period lists at 3, 4, 2 and 1 columns. Each is one continuous 320px scroll area with the year numbers as headings inside it, opening on the active year.
CalendarPreview.Label / CalendarPreview.Separator
The field label above the input, and the rule between the switcher and the view.
CalendarPreview.Footer
The row below the calendar. A bare string is wrapped in Text; anything else renders as given.
It needs no container of its own: the root renders a column that hugs its content, so .Days and .Footer stack whatever the surrounding layout does.
useCalendar
Reads the enclosing root's state, for building parts the library does not ship. Deliberately narrow:
1import { useCalendar } from '@raystack/apsara'23const { value, setValue, scale, setScale, month, setMonth, isDateUnavailable } = useCalendar()
Calling it outside a CalendarPreview throws, naming the part that asked.
Slots
Every rendered part carries a stable data-slot attribute for styling and testing:
| Slot | Element |
|---|---|
calendar-preview | The root, a column wrapping the parts |
calendar-preview-trigger | The popover anchor |
calendar-preview-content | The portaled popover surface |
calendar-preview-input | The typeable date field |
calendar-preview-days | The day view surface |
calendar-preview-header | The header row, single-month layout |
calendar-preview-month-header | One month's header, when several months are shown |
calendar-preview-caption | The month label |
calendar-preview-caption-popup | The month and year scroller (when dropdown is open) |
calendar-preview-caption-months | The month column of the scroller |
calendar-preview-caption-month | One month in the scroller |
calendar-preview-caption-years | The year column of the scroller |
calendar-preview-caption-year | One year in the scroller |
calendar-preview-reset | The reset button |
calendar-preview-prev-month | The previous-month button |
calendar-preview-next-month | The next-month button |
calendar-preview-grid | The grid root |
calendar-preview-weeks | Wrapper around the table and its skeleton |
calendar-preview-table | The <table> that holds the days |
calendar-preview-skeleton | The loading skeleton shown over the grid |
calendar-preview-weekday | One weekday heading |
calendar-preview-day | The <button> for a single day |
calendar-preview-day-number | The day number inside a day button |
calendar-preview-day-info | Content above the number (when dateInfo resolves) |
calendar-preview-day-tooltip | The tooltip shown on hover |
calendar-preview-body | The popup body |
calendar-preview-label | The field label |
calendar-preview-scales | The scale switcher |
calendar-preview-scale | One scale chip |
calendar-preview-separator | The rule below the switcher |
calendar-preview-panel | The view container |
calendar-preview-months / -quarters / -half-years / -years | One period list |
calendar-preview-period-group | One year's block inside a period list |
calendar-preview-period-year | The year heading |
calendar-preview-period | One period cell |
calendar-preview-footer | The footer row |
calendar-preview-footer-text | The Text wrapping a string footer |
Day cells also carry their state, so a stylesheet can target it without a class:
| Attribute | Set when |
|---|---|
data-selected | The day is the committed value |
data-draft | The day has roving focus but is not committed |
data-unavailable | The day is out of bounds or rejected by isDateUnavailable |
data-today | The day is today |
data-outside | The day belongs to an adjacent month |
data-scale | The granularity the value is committed at |
Examples
Composition
Each part renders a default; children replace it.
1<CalendarPreview defaultMonth={new Date(2024, 3, 1)}>2 <CalendarPreview.Days />3</CalendarPreview>
Reset
.Reset restores defaultDate and leaves the visible month alone — it is a value reset, not a view reset. It renders only when defaultDate is set and the current value differs from it, so the button disappears once there is nothing to restore.
defaultDate is a separate prop from defaultValue because defaultValue is ignored once value is passed. Keying the reset off its own prop is what makes it work for a controlled calendar.
1<CalendarPreview2 defaultMonth={new Date(2024, 3, 1)}3 defaultDate={new Date(2024, 3, 17)}4 defaultValue={new Date(2024, 3, 24)}5>6 <CalendarPreview.Days />7</CalendarPreview>
Selection bounds
minDate, maxDate and isDateUnavailable disable cells. None of them clamps navigation — the chevrons and the scroller still reach any month. Bounds compare whole calendar days, so a minDate carrying a time of day still leaves its own day selectable.
1<CalendarPreview2 defaultMonth={new Date(2024, 3, 1)}3 minDate={new Date(2024, 3, 17)}4>5 <CalendarPreview.Days />6</CalendarPreview>
Grid layout
Outside days are off by default, so a grid ends on the last day of its month with the leading cells blank.
1<CalendarPreview defaultMonth={new Date(2024, 3, 1)}>2 <CalendarPreview.Days>3 <CalendarPreview.Header />4 <CalendarPreview.Grid showOutsideDays />5 </CalendarPreview.Days>6</CalendarPreview>
Date information and tooltips
dateInfo and tooltipMessages are functions of the date, not records keyed by a formatted string. dateInfo content renders above the day number; today's dot sits below it, so the two never collide.
1<CalendarPreview defaultMonth={new Date(2024, 3, 1)}>2 <CalendarPreview.Days>3 <CalendarPreview.Header />4 <CalendarPreview.Grid5 dateInfo={(date) =>6 date.getDate() % 7 === 0 ? (7 <Text size="micro" variant="accent">8 $9 </Text>10 ) : null11 }12 />13 </CalendarPreview.Days>14</CalendarPreview>
Month and year scroller
<CalendarPreview.Caption dropdown /> turns the caption into a filled chip that opens two adjacent scrolling columns. It is a plain popover of buttons, not a Select — picking from either column moves the view and never selects a value.
Date picker
The date picker is not a separate export — it is this composition:
1<CalendarPreview value={date} onValueChange={setDate}>2 <CalendarPreview.Trigger>3 <CalendarPreview.Input />4 </CalendarPreview.Trigger>5 <CalendarPreview.Content>6 <CalendarPreview.Days />7 </CalendarPreview.Content>8</CalendarPreview>
The popover opens when the input takes focus. Enter, blur and an outside click all commit — there is no Apply button. Dismissal is Base UI's, so escape and outside press behave like every other popover in the library.
"Without calendar icon" is composition rather than a prop: pass trailingIcon={null} to .Input.
1<CalendarPreview defaultMonth={new Date(2024, 3, 1)}>2 <CalendarPreview.Trigger>3 <CalendarPreview.Input />4 </CalendarPreview.Trigger>5 <CalendarPreview.Content>6 <CalendarPreview.Days />7 </CalendarPreview.Content>8</CalendarPreview>
Range selection
selection="range" turns clicks into endpoints. Give each .Input a field:
1<CalendarPreview selection="range" value={range} onValueChange={setRange}>2 <CalendarPreview.Trigger>3 <CalendarPreview.Input field="start" />4 <CalendarPreview.Input field="end" />5 </CalendarPreview.Trigger>6 <CalendarPreview.Content>7 <CalendarPreview.Days numberOfMonths={2} />8 </CalendarPreview.Content>9</CalendarPreview>
onValueChange fires on a complete range or not at all. to is not nullable, so there is no partial { from?, to? } to gate on. The half-built range stays internal — the grid styles the track from it, but nothing is emitted until the second endpoint lands.
The click machine:
| State | A click does |
|---|---|
| Nothing selected | sets from, moves focus to the end field |
from only, later day | completes the range, emits, closes the popover |
from only, earlier day | that day becomes the new from |
| Complete range | restarts — the new day is from, and the value stays at the previous range until the new one completes |
Completing asks the popover to close through onOpenChange, so a consumer holding open open is not fought.
Instead of a lock prop, mark one endpoint's .Input as readOnly — the grid will not rewrite it. A read-only endpoint with no value makes the range unsatisfiable: the free endpoint sets, the range never completes, and nothing emits. Give a read-only endpoint a value.
1<CalendarPreview selection="range" defaultMonth={new Date(2024, 3, 1)}>2 <CalendarPreview.Trigger>3 <Flex align="center" gap={3}>4 <CalendarPreview.Input field="start" />5 <CalendarPreview.Input field="end" />6 </Flex>7 </CalendarPreview.Trigger>8 <CalendarPreview.Content>9 <CalendarPreview.Days numberOfMonths={2} />10 </CalendarPreview.Content>11</CalendarPreview>
Scale-aware selection
Pass scales to select at granularities coarser than a day. A single value hides the switcher; anything more shows it.
1<CalendarPreview scales={['day', 'month', 'quarter', 'halfYear', 'year']}>2 <CalendarPreview.Trigger placeholder="Add start date" />3 <CalendarPreview.Content>4 <CalendarPreview.Body />5 </CalendarPreview.Content>6</CalendarPreview>
1<CalendarPreview2 scales={["day", "month", "quarter", "halfYear", "year"]}3 defaultMonth={new Date(2026, 7, 1)}4 defaultScale="quarter"5>6 <CalendarPreview.Body />7</CalendarPreview>
The value carries its scale
A Date cannot say whether it means "August 2026" or "1 August 2026", so beyond day scale the value is a ScaleValue:
1interface ScaleValue { date: 'YYYY-MM-DD'; scale: Scale }
scales | value |
|---|---|
omitted, or 'day' | Date — unchanged |
| any other scale, or any array | ScaleValue |
date is stored as YYYY-MM-DD because lexicographic order is chronological order, which is what lets bounds compare without parsing. It is never what you see — every trigger, input and annotation renders through formatValue, which is DD MMM YYYY at day scale and the period's own shorthand above it. onValueChange's details carry toDate() if you want a Date.
Switching scale drafts, it does not emit
Moving between scales moves the view and sets a draft. Nothing is emitted until a cell is clicked or Enter is pressed; Escape drops the draft and restores the input from value.
trailingValue picks the edge
A period has two edges, and which one a field means depends on the field. trailingValue emits the period's last day rather than its first — "July 2026" becomes 2026-07-31 instead of 2026-07-01. It changes the value, not the formatting, and it is month-end correct: February 2028 trailing is 2028-02-29.
That also decides availability, which tests the date a period would produce. Bounded at 15 July 2026:
| Period | A start field emits | An end field emits | Start | End |
|---|---|---|---|---|
| H1 2026 | 1 Jan | 30 Jun | disabled | disabled |
| July 2026 | 1 Jul | 31 Jul | disabled | available |
| Q3 2026 | 1 Jul | 30 Sep | disabled | available |
Every one of those periods starts before the bound. Only the produced date separates them.
A start/end pair is two roots
Not selection="range". Each end has its own scales and trailingValue, and they can hold different scales — "1 Aug 2026 → Q3 2026" is not expressible as one range value. The consumer owns the pair and any from <= to check.
1<Flex align="center" gap={3}>2 <CalendarPreview3 scales={["day", "month", "quarter", "halfYear", "year"]}4 defaultValue={{ date: "2026-08-01", scale: "day" }}5 >6 <CalendarPreview.Trigger placeholder="Add start date" />7 <CalendarPreview.Content>8 <CalendarPreview.Body />9 </CalendarPreview.Content>10 </CalendarPreview>1112 <Text size="small" variant="secondary">13 →14 </Text>15
Accessibility
- Arrow keys move between days; the focused cell carries
data-draftuntil it is committed - Each grid is labelled with its month, so the caption is not the only announcement
- Nav buttons carry
aria-label, and the scroller's columns are labelled groups - Selected and unavailable days are announced through their native button state