Calendar Preview

A subcomposed calendar that owns its selection and view state.

A calendar that owns its selection and view state, composed from parts you mount only as deep as you need.

1import { CalendarPreview } from '@raystack/apsara'

Usage

1<CalendarPreview>
2 <CalendarPreview.Days />
3</CalendarPreview>

Examples

Calendar

The day view. Every layout option lives on .Grid, so two grids can differ.

1<CalendarPreview defaultMonth={new Date(2024, 3, 1)}>
2 <CalendarPreview.Days />
3</CalendarPreview>

Date picker

A trigger wrapping an .Input, with the day view in a popover.

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 picker

selection="range" turns clicks into endpoints. Give each .Input a field.

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>

Time periods

scales selects at granularities coarser than a day.

1<CalendarPreview
2 scales={["day", "month", "quarter", "halfYear", "year"]}
3 defaultMonth={new Date(2026, 7, 1)}
4 defaultScale="quarter"
5>
6 <CalendarPreview.Body showIcon />
7</CalendarPreview>

Limits

minDate, maxDate and isDateUnavailable disable cells.

None of them clamps navigation — the chevrons and the scroller still reach any month. isDateUnavailable is day scale only; period cells are bounded by minDate and maxDate instead.

1<CalendarPreview
2 defaultMonth={new Date(2024, 3, 1)}
3 minDate={new Date(2024, 3, 17)}
4>
5 <CalendarPreview.Days />
6</CalendarPreview>

States

disabled makes the whole calendar inert; readOnly keeps it focusable.

1<CalendarPreview defaultMonth={new Date(2024, 3, 1)} disabled>
2 <CalendarPreview.Trigger>
3 <CalendarPreview.Input />
4 </CalendarPreview.Trigger>
5 <CalendarPreview.Content>
6 <CalendarPreview.Days />
7 </CalendarPreview.Content>
8</CalendarPreview>

Validation

Typed dates are checked on every keystroke, and a date that fails is never committed.

Blurring or pressing Enter on a date that does not resolve leaves the typed text in the field. The field stays marked invalid but now shows something other than the committed value — read the value from onValueChange, never from the input's text.

1(function CalendarPreviewInvalidExample() {
2 const [error, setError] = React.useState();
3
4 return (
5 <Flex justify="center">
6 <Field label="Start date" error={error}>
7 <CalendarPreview
8 defaultMonth={new Date(2024, 3, 1)}
9 minDate={new Date(2024, 3, 1)}
10 maxDate={new Date(2024, 3, 30)}
11 >
12 <CalendarPreview.Trigger>
13 <CalendarPreview.Input
14 errorMessages={{ unparseable: "Use DD MMM YYYY" }}
15 onValidityChange={({ message }) => setError(message)}

Reset

Restores defaultDate. Stays visible but disabled when there is nothing to restore.

defaultDate={null} clears the selection and reports reason: 'clear'; omitting it hides the button.

1<CalendarPreview
2 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>

Customising

Children replace the content a part computes from context.

1<CalendarPreview defaultMonth={new Date(2024, 6, 1)}>
2 <CalendarPreview.Days>
3 <CalendarPreview.Header>
4 <CalendarPreview.Caption>Delivery date</CalendarPreview.Caption>
5 <CalendarPreview.Caption />
6 <CalendarPreview.PrevMonth />
7 <CalendarPreview.NextMonth />
8 </CalendarPreview.Header>
9 <CalendarPreview.Grid />
10 </CalendarPreview.Days>
11</CalendarPreview>

Anatomy

1<CalendarPreview>
2 <CalendarPreview.Trigger>
3 <CalendarPreview.Input />
4 </CalendarPreview.Trigger>
5
6 <CalendarPreview.Content>
7 <CalendarPreview.Body>
8 <CalendarPreview.Label />
9 <CalendarPreview.Input />
10 <CalendarPreview.Scales>
11 <CalendarPreview.Scale value="day" />
12 </CalendarPreview.Scales>
13 <CalendarPreview.Reset />
14 <CalendarPreview.Separator />
15
16 <CalendarPreview.Panel>
17 <CalendarPreview.Days>
18 <CalendarPreview.Header>
19 <CalendarPreview.Caption />
20 <CalendarPreview.Reset />
21 <CalendarPreview.PrevMonth />
22 <CalendarPreview.NextMonth />
23 </CalendarPreview.Header>
24 <CalendarPreview.Grid />
25 </CalendarPreview.Days>
26 <CalendarPreview.Months />
27 <CalendarPreview.Quarters />
28 <CalendarPreview.HalfYears />
29 <CalendarPreview.Years />
30 </CalendarPreview.Panel>
31 </CalendarPreview.Body>
32 </CalendarPreview.Content>
33
34 <CalendarPreview.Footer />
35</CalendarPreview>

.Reset appears twice because exactly one of the two renders: .Header holds it at day scale, .Body at every coarser scale, where there is no .Header.

Every part renders its own default, so none of this is required. .Grid renders the day cells itself and takes no children — .Day and .Weekday are overrides passed through components:

1<CalendarPreview.Grid components={{ DayButton: MyDay, Weekday: MyWeekday }} />

API reference

CalendarPreview

The root. Owns the selected value and the visible month. Also takes render, className and ref.

Prop

Type

CalendarPreview.Trigger

Anchors the popover and owns opening it. Renders the formatted value, or the placeholder, when given no children.

Prop

Type

CalendarPreview.Content

The portaled popover surface. Takes Popover.Content props — side, align, sideOffset — and flips above the trigger on collision.

CalendarPreview.Input

The typeable date field.

Prop

Type

CalendarPreview.Body

The popup body: label, input, scale switcher and the view for the active scale.

Prop

Type

CalendarPreview.Days

The day view — a header and a grid.

Prop

Type

CalendarPreview.Grid

The day grid. Layout and per-day data live here rather than on the root.

Prop

Type

CalendarPreview.Header

The row above the grid. Composes .Caption, .Reset, .PrevMonth and .NextMonth when given no children.

Prop

Type

CalendarPreview.Caption

The month label above the grid, and optionally the trigger for the month and year scroller.

Prop

Type

CalendarPreview.PrevMonth / CalendarPreview.NextMonth

Step the view one month.

Prop

Type

CalendarPreview.Reset

Restores defaultDate, reporting reason: 'reset'. Carries data-restored while there is nothing to restore.

Prop

Type

CalendarPreview.Scales / CalendarPreview.Scale

The scale switcher, built on Apsara Tabs. Renders nothing when only one scale is offered. .Scale is only needed to relabel or reorder.

Prop

Type

CalendarPreview.Panel

The view container. Mounts all five views; each gates on the active scale itself.

CalendarPreview.Months / .Quarters / .HalfYears / .Years

Year-grouped period lists at 3, 4, 2 and 1 columns, opening on the active year.

CalendarPreview.Label / CalendarPreview.Separator

The field label above the input, and the rule between the switcher and the view. .Label renders nothing without children.

CalendarPreview.Footer

The row below the calendar. A bare string is wrapped in Text; anything else renders as given.

Prop

Type

useCalendar

Reads the enclosing root's state, for building parts the library does not ship. Throws outside a CalendarPreview, naming the part that asked.

1import { useCalendar } from '@raystack/apsara'
2
3const { value, setValue, scale, draft, scaleDraft, month, setMonth, isDateUnavailable } =
4 useCalendar()

Prop

Type

details (onValueChange)

Prop

Type

Behaviour

Value shape

scalesvalue
omitted, or 'day'Date
any other scale, or any arrayScaleValue
1interface ScaleValue { date: 'YYYY-MM-DD'; scale: Scale }

date is stored as YYYY-MM-DD so lexicographic order is chronological order. It is never what you see — every trigger, input and annotation renders through formatValue.

Drafting

ActionResult
Switch scalemoves the view, sets a draft, emits nothing
Click a cell, or press Entercommits the draft
Escapedrops the draft, restores the input from value
Range, one endpointstays internal; the grid styles the track from it

Range clicks

StateA click does
Nothing selectedsets from, moves focus to the end field
from only, later daycompletes the range and emits
from only, earlier daythat day becomes the new from
Complete rangerestarts — the new day is from

onValueChange fires on a complete range or not at all. Typing is stricter than clicking: an endpoint that crosses its partner is rejected as out-of-order rather than restarting.

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.

Validation reasons

reasonMeans
unparseableThe text is not a date the input could read at all
out-of-boundsA real date, outside minDate / maxDate
unavailableA real date in range that isDateUnavailable rejected
out-of-orderRange only — the endpoint crossed its partner

onValidityChange fires only when validity changes, and carries a ready-to-render message — undefined while valid, which is what Field's error wants. Override per reason with errorMessages.

trailingValue

Emits a period's last day rather than its first, and is month-end correct. It changes the value, not the formatting. Availability tests the date a period would produce, here with minDate={15 Jul 2026}:

PeriodA start field emitsAn end field emitsStartEnd
H1 20261 Jan30 Jundisableddisabled
July 20261 Jul31 Juldisabledavailable
Q3 20261 Jul30 Sepdisabledavailable

A start/end pair is two roots, not selection="range" — each end has its own scales and trailingValue, and they can hold different scales.

Styling

Slots and state attributes

Every rendered part carries a stable data-slot attribute for styling and testing:

SlotElement
calendar-previewThe root, a column wrapping the parts
calendar-preview-triggerThe popover anchor
calendar-preview-contentThe portaled popover surface
calendar-preview-inputThe typeable date field
calendar-preview-daysThe day view surface
calendar-preview-headerThe header row, single-month layout
calendar-preview-month-headerOne month's header, when several months are shown
calendar-preview-captionThe month label, single-month layout
calendar-preview-month-header-captionOne month's label, when several months are shown
calendar-preview-caption-positionerThe scroller's positioning wrapper
calendar-preview-caption-popupThe month and year scroller (when dropdown is open)
calendar-preview-caption-monthsThe month column of the scroller
calendar-preview-caption-monthOne month in the scroller
calendar-preview-caption-dividerThe rule between the scroller's two columns
calendar-preview-caption-yearsThe year column of the scroller
calendar-preview-caption-yearOne year in the scroller
calendar-preview-resetThe reset button
calendar-preview-prev-monthThe previous-month button
calendar-preview-next-monthThe next-month button
calendar-preview-gridThe grid root
calendar-preview-weeksWrapper around the table and its skeleton
calendar-preview-tableThe <table> that holds the days
calendar-preview-skeletonThe loading skeleton shown over the grid
calendar-preview-weekdayOne weekday heading
calendar-preview-week-numberOne week-number cell (when showWeekNumber)
calendar-preview-week-number-headerThe week-number column heading
calendar-preview-day-triggerThe tooltip trigger wrapping each day button
calendar-preview-dayThe <button> for a single day
calendar-preview-day-numberThe day number inside a day button
calendar-preview-day-infoContent above the number (when dateInfo resolves)
calendar-preview-day-tooltipThe tooltip shown on hover
calendar-preview-bodyThe popup body
calendar-preview-labelThe field label
calendar-preview-scalesThe scale switcher
calendar-preview-scaleOne scale chip
calendar-preview-separatorThe rule below the switcher
calendar-preview-panelThe view container
calendar-preview-months / -quarters / -half-years / -yearsOne period list
calendar-preview-period-groupOne year's block inside a period list
calendar-preview-period-yearThe year heading, on every view but .Years
calendar-preview-periodOne period cell
calendar-preview-footerThe footer row
calendar-preview-footer-textThe Text wrapping a string footer

Day cells also carry their state:

AttributeSet when
data-selectedThe day is the committed value
data-draftThe day has roving focus but is not committed
data-unavailableThe day is out of bounds or rejected by isDateUnavailable
data-todayThe day is today
data-outsideThe day belongs to an adjacent month
data-scaleThe granularity the value is committed at

Accessibility

  • Arrow keys move between days; the focused cell carries data-draft until it is committed
  • readOnly is conveyed with aria-readonly on the grid and aria-disabled on each day, and the grid stays focusable and arrow-navigable — unlike disabled
  • 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

Notes

Performance. dateInfo, tooltipMessages and isDateUnavailable are functions, so an inline arrow re-renders every day cell. Wrap them in useCallback or hoist them out.

Localization. English only — month and weekday names come from date-fns' en-US, and the nav, reset and caption labels are hardcoded. timeZone is unaffected.

Migrating from Calendar

Not a drop-in replacement. Two props keep their names and change their meaning:

PropOn CalendarOn CalendarPreview
disabledA day matcherA boolean that makes the whole calendar inert. Use isDateUnavailable or minDate / maxDate for days
showOutsideDaysDefaults to trueDefaults to false

The rest are renames:

CalendarCalendarPreview
selectedvalue
onSelectonValueChange
startMonth / endMonthminDate / maxDate
disabled (matcher)isDateUnavailable
loadingDataloading
dateFormatformatValue
requiredclearable (inverted)
footer prop<CalendarPreview.Footer> part
captionLayout="dropdown"<CalendarPreview.Caption dropdown />

Slot names changed too:

Calendar slotCalendarPreview slot
calendar-grid-tablecalendar-preview-table
calendar-grid-skeletoncalendar-preview-skeleton
calendar-month-gridcalendar-preview-weeks
calendar-nav-previouscalendar-preview-prev-month

Two things have no replacement: the record forms of dateInfo and tooltipMessages (both are functions here), and the classNames escape hatch — style through data-slot instead.