Cal.comdesign-system

Booker Calendar

Anatomy, states, and interaction rules for the month-view Booker's calendar pane.

This page is a migration blueprint for the month-view Booker's calendar pane. Cal defines the supported booking flows; the previews show the target Coss composition. Pair it with Booker Event Information when assembling the Booker.

Examples use synthetic availability and start with “today” in September 2026. They make no API requests. This is an interactive migration reference, not a production-ready calendar replacement. Week and column layouts, the time-slot list, and the booking form are outside this page's scope.

Anatomy

The map shows the calendar's parts in order, with their intended spacing. Each label links to its section. The calendar is a plain middle pane: background, outer border, shadow, and corners belong to the enclosing Booker, not an additional calendar card. The layout selector applies to the map and all previews below.

The layout depends on the whole Booker's width, not the isolated calendar's width. The map's layout selector exposes all three atom arrangements:

Web app means the feature exists in Cal; explicit target rules identify deliberate behavior changes. API v2 means it is possible with API v2 data and caller configuration, not that the current atom already implements it. Partial identifies a contract gap. See Data contracts for the required mappings.

Month and navigation

Web appAPI v2

The heading shows the displayed month and year. Previous and next move one month at a time; controls are disabled at known boundaries. Changing month clears a selected time and chooses the first selectable date in the new view, unless an existing selection remains visible and valid or the mobile flow requires an explicit date action.

Month and navigation
September 2026
September 2026

Use the attendee's timezone for “today” and the event's resolved booking-window rules for navigation bounds. Cal's ROLLING_WINDOW counts available days; ordinary ROLLING counts calendar or business days. Do not collapse these into the same date calculation. Navigation limits are not a replacement for disabling individual dates outside the exact booking window.

Only disable next-month navigation at a known booking boundary. An unlimited event has no last month, and an unfetched month is not an unavailable month. The owner supplies resolved bounds separately from the range already loaded; November in the basic examples is a fixture boundary, not a product limit.

Both boundaries reached

When all bookable dates fall in the displayed month, both navigation controls are disabled. In this example the permitted range is September 15–25.

Single-month booking window
September 2026
September 2026

No upper booking boundary

This navigation-only example has no maximum date. Next stays available beyond November; the real owner must request each newly viewed range.

Unbounded next-month navigation
September 2026
September 2026

Weekdays

Web appAPI v2

Use Cal's selected UI language for names and the host's configured first day of the week for ordering. The weekday row and date grid must share that order. Date labels remain Gregorian, including when the language uses different numerals.

Sunday first
September 2026
SunMonTueWedThuFriSat
Monday first · Italian
settembre 2026
lunmarmergiovensabdom

Day cells

Web appAPI v2

A date is bookable when the resolved availability response contains at least one offered slot. The server owns notice periods, buffers, host eligibility, seats, and booking limits. Do not infer availability from the weekly schedule alone. An out-of-office day is a separate state described below.

Available

An available date uses a muted fill. Hover and keyboard focus use the same primary ring. Selecting it updates the date and clears any previously selected time; selecting it again keeps it selected.

Available date

Unavailable

Past dates, dates outside the booking window, and dates with no offered slots use no fill, lighter text, and normal weight. They cannot be activated. Availability still loading must use a loading state, not an unavailable date.

Unavailable date

Today

The dot identifies today in the attendee's timezone. It does not imply that the date is available. Update this reference when the timezone changes or the session crosses midnight.

Today
Today, unavailable

Selected

The selected date uses the primary fill and contrasting text. Selection is separate from keyboard focus and from a selected time slot.

Selected date
Today, selected

Out of office

Web appAPI v2 · Partial

When every entry for a day is an absence, Cal displays the absence emoji instead of the day number. The accessible label must still include the full date and “Out of office.” A day with both real slots and absence entries keeps its regular day number; the time-slot area handles the partial-day information.

Required extension: The current Coss atom has no absence day state. These previews apply Cal's behavior to the atom's day styling. Preserve absence metadata separately from real bookable slots.

No public information

With neither showNotePublicly enabled nor an alternative host, the absence day is disabled. Private note text must never enter public preview data or the public UI.

Out of office · disabled

Public note or alternative host

This day remains selectable when showNotePublicly is enabled or an alternative host exists, so the time-slot area can show absence information or offer that host. The flag controls selection even when the note text is empty; do not use a nonempty note as the condition. Render note text only when it exists and its public flag is enabled. Selecting an absence must not create a bookable slot from its marker.

Out of office · public note
Out of office · public note enabled, no text

The out-of-office example shows disabled absences, public notes, and alternative-host information together.

Day grid

Web appAPI v2

Reserve six rows so the calendar height stays stable between months. Leading cells before the month and unused trailing cells stay empty. They are not selectable dates and must not receive keyboard focus.

Regular month grid
September 2026

Shifted weeks

For the current month, once today reaches the fourth row, extend the grid into the next month so today's week and two following weeks remain visible. Drop early weeks only when necessary to stay within six rows. Future months keep their regular month grid. This is the target atom behavior; Cal currently uses a different cutoff and drops the first seven dates.

Late September · today is September 28
September 2026
September 2026
SunMonTueWedThuFriSat

Selected: September 28, 2026

Next-month days

The first visible next-month day includes a short month label. Available overflow dates show the full month on hover or keyboard focus with no tooltip delay. Their selection and keyboard behavior are the same as other available dates.

First day of the next month

Target selection rule: Keep the displayed month when an overflow date is selected, as Cal's monthly Booker does. The current atom hook switches months; this blueprint deliberately keeps selection independent from the displayed month. The previous interactive example demonstrates this rule.

Fetch every visible date, including overflow. An empty response and a date outside the fetched range are different states.

Loading

Web appAPI v2

Initial loading replaces the heading, weekday labels, and visible day cells. There is no actionable date until availability is known.

Initial loading
Loading availability

While fetching another range or recalculating after a duration/timezone change, keep the heading and weekday labels and replace the date cells. Announce the update without moving focus into the grid. Background refresh can retain valid cached data; loading must not temporarily announce an empty month.

Availability loading
Loading availability
September 2026
SunMonTueWedThuFriSat

Availability lifecycle

These controls simulate requests rather than calling an API. Use them to inspect loading, refresh, context changes, errors, and late responses. A response must match the active request context before it can update the calendar; a refresh must revalidate selection without treating pending data as an empty result.

Select a sample time, then remove the selected day with Refresh. Change duration repeatedly to test overlapping requests; the final duration must win. Fail a request and retry, or advance “today” to revalidate past dates. These controls and their feedback are documentation chrome, not calendar UI.

Interactive availability lifecycle
Loading availability
September 2026
SunMonTueWedThuFriSat
Updating availability… Today: 2026-09-08. Duration: 30 minutes. Selected: 2026-09-23. No time selected.

Empty and error outcomes

These messages belong in the time-slot area in the target composition, not over the calendar. They are included here because navigating the calendar determines which outcome is shown. Keep navigation available whenever another permitted range can be checked.

Deliberate change: Cal currently auto-advances once when its initial current month is empty and can open a no-availability dialog. This blueprint follows the atom's explicit outcome and action instead: do not silently advance the month or open that dialog alongside these messages.

The owning Booker must resolve state before choosing an outcome: a pending reschedule host choice takes priority; loading is not empty; a failed request shows an error; a selected public absence shows absence information. Only a successful, relevant response can establish an empty result. Preserve Cal's suppression of no-availability interruptions in settings preview and while the reschedule host-choice dialog is open.

Calendar · no available dates
September 2026
September 2026
SunMonTueWedThuFriSat

No date selected

Time-slot area · no slots in the checked range
No available times
No open slots in September. Availability beyond this month has not been checked.

When a later available date is already known, use the atom's “View first availability” action to select it. When later dates have not been loaded, offer to check the next range. Do not claim that the event has no future slots based only on a limited cache.

Time-slot area · later availability known
No available times
No slots on this date. The next available date is September 14.
Time-slot area · booking window ended
No available times
This event accepts bookings through September 25. No dates remain in that window.

An ended window has no next-range action. A failed request offers retry and must not be presented as unavailable dates. These buttons report the callback they would request; they do not fetch slots.

Time-slot area · request failed
Availability unavailable
We couldn't load availability. Try again.

Interaction rules

The preview feedback underneath complete calendars describes state changes. It is documentation chrome, not an extra element in the production calendar pane.

Keyboard and right-to-left

Tab enters the grid at one selectable date, including a visible overflow date. Arrow keys move by day or week and skip unavailable dates. Home/End move to the first/last selectable date of the week. Page Up/Down move a month and clamp the day at month end; Shift moves a year. Enter/Space select the focused date. Left/right arrows follow the visual direction in RTL. Keyboard focus alone does not change the selected date or time, even when it moves into another month.

An unknown destination requests its range before restoring focus in the original navigation direction. If the range resolves empty, focus the grid rather than an unavailable date. Cancel pending focus if the user moves to another control or clicks elsewhere; late responses must not steal focus.

The previews correct the atom's month-overflow and disabled-week-boundary keyboard cases. These are target behaviors for both the app migration and the atom; documenting them here does not update either production implementation.

Arabic · Saturday first · keyboard navigation
سبتمبر 2026
سبتمبر 2026
السبتالأحدالاثنينالثلاثاءالأربعاءالخميسالجمعة

Selected: 8 سبتمبر 2026

Complete examples

Standard month

September 8 is today. Weekdays have synthetic slots; weekends do not. Navigate through November to check selection and stable grid height.

Standard calendar pane
September 2026
September 2026
SunMonTueWedThuFriSat

Selected: September 8, 2026

Preserved date

The attendee arrived with September 23 selected. It stays selected even though earlier dates are available.

Existing selection
September 2026
September 2026
SunMonTueWedThuFriSat

Selected: September 23, 2026

Restricted booking window

Only September 15–25 is permitted. Boundary dates remain eligible if they have slots; both month controls are disabled.

Date-range restriction
September 2026
September 2026
SunMonTueWedThuFriSat

Selected: September 15, 2026

With out-of-office days

September 10 is disabled, September 11 exposes a public note, and September 14 offers another host. Select the latter two to inspect their different outcomes.

Out-of-office calendar pane
September 2026
September 2026
SunMonTueWedThuFriSat

Selected: September 8, 2026

Explicit mobile selection

This demonstrates Cal's optional mobile embed behavior, not every mobile booking page. No day is selected on entry or month navigation. Select a date to trigger the time-slot step; use Back to return. The callback is simulated here; modal opening, focus return, and ordinary mobile auto-scroll remain the surrounding Booker's responsibility.

Mobile two-step selection
September 2026
September 2026
SunMonTueWedThuFriSat

Choose a date to open the time-slot step.

Data contracts

Keep the calendar controlled by the owning Booker. BookerCalendarPreview is a docs-only reference under apps/design-system/components, not a new production component or API adapter.

Performance and data loading

These rules belong to the owning Booker's data layer, not the calendar renderer. The atom fetches three-month ranges, warms a later range in the background, shares in-flight requests, and reuses cached availability, initial data, and resolved event metadata. Preserve fast navigation through Cal's existing query cache rather than adding a second independent cache.

  • Load the visible range first. Include overflow dates. Bound background fetching by the booking window and a limited lookahead/concurrency budget; respect readiness and embed fetch guards. A failed prefetch must not replace a usable calendar with an error and can be retried when needed.
  • Reuse matching data. Navigation into a fresh, covered range should not repeat its request. Prefetch into the cache the next view actually reads; share overlapping in-flight work and reuse initial data only when its context matches. Avoid reloading unchanged event metadata on navigation.
  • Keep availability fresh. Define expiry and cache retention; preserve the tRPC path's focus refresh, periodic refresh, and targeted invalidation. Do not copy the atom's indefinite covered-month cache. Cached availability never replaces selected-slot checks or final server validation.
  • Key by the full booking context. Include the range and every availability-affecting request input, including effective timezone, duration, reschedule booking/host preference, and routing/host filters. Do not copy the older API path's incomplete manual key. Ignore obsolete-context responses; cancel obsolete requests where supported.
  • Keep transitions stable. Follow Loading: no empty-state flash while fetching, and no unnecessary skeleton when usable cached data exists. A background refresh must still revalidate the selected date and time.
  • Avoid repeated date work. Reuse locale/timezone formatters and derive the grid and slot index when their inputs change, not for every unrelated render. Use lightweight calendar-date arithmetic without losing timezone or DST correctness.

Measure before choosing the prefetch budget. Three months is the atom's strategy, not a migration requirement. Compare cold-load time, cached navigation latency, request count, response size, and server work with and without warming. Include rapid navigation, context changes, and large collective/round-robin events. The availability lifecycle demo simulates request correctness, not real prefetching or performance.

Production integration still required

The page covers the target calendar surface; migration is complete only after the owning Booker is wired and verified:

  • Connect real availability, booking-window bounds, public absence metadata, and every supported reschedule/routing context.
  • Preserve URL/store selection, exact slot timestamps, context-specific cache identity, stale-response rejection, invalidation, and timezone/midnight updates.
  • Integrate the time-slot surface, host-choice priority, mobile modal/scroll behavior, keyboard focus recovery, and screen-reader announcements through loading and refresh.
  • Carry forward custom branding, embed events, settings-preview overrides, overlay-calendar controls, and timezone access when event details are hidden.
  • Verify these flows in the app, including all three Booker layouts. Synthetic previews do not replace product integration tests.

Implementation references

Current product entry points include personal booking pages (/<username>/<event-slug>) and team booking pages (/team/<team-slug>/<event-slug>), including their embedded month-view variants. These are examples, not an exhaustive route inventory.