Cal.comdesign-system

Booker Time Slots

Components, state priority, and workflows for the month-view Booker's time-slot pane.

This page is a migration blueprint for the month-view Booker's right time-slot pane. Cal defines the supported flows and display conditions; the previews show the target Coss composition. Use it alongside Booker Event Information and the calendar blueprint when assembling the Booker.

Examples use synthetic dates and availability. They make no requests or bookings. Week and column layouts, the booking form, and production API/tRPC integration are outside this page's scope.

Anatomy

The map shows the parts in Cal's order. Each label links to its section. Conditional parts and replacement states are combined for reference; they do not all appear together in a booking flow.

The time-slot area is a plain rail inside the shared Booker card. Its leading divider becomes a top divider on mobile; background, outer corners, and shadow belong to the enclosing Booker. There is no additional card around the slot list. The header and optional reschedule controls stay above the independently scrolling list, which uses ScrollArea with edge fades.

The component map includes the surrounding card's edge: rounded-2xl trailing corners with a square leading edge on desktop, and rounded bottom corners with a square top edge on mobile. Its hatched controls use the canonical rounded-lg shapes and slot spacing; inline confirmation sits beside its selected slot.

The selector applies to every preview. Production breakpoints depend on the whole Booker's container width, not the width of this isolated rail:

Controls use the viewport's sm breakpoint: slot buttons are 36px high below 640px and 32px above it; header rows are 40px and 36px respectively. Mobile previews simulate the stacked container and small-viewport slot/header sizes; the time-format selector uses Coss's segmented control. Header-to-list spacing is 8px, then 4px list-top padding. The 430px desktop preview height stands in for the surrounding Booker's height; it is not a production height requirement. Seat and status rows can grow beyond the ordinary button height.

Components

Build one stable pane from the components below. Conditions change whether a component is present or which documented variant it renders; they do not create separate pane implementations. The dashed border marks an isolated component preview, matching the Booker Event Information documentation. It is documentation chrome, not production UI.

Web app badges mean Cal supports the behavior. API v2 means the public contract supplies the necessary data. Partial marks a gap; Local state identifies presentation and interaction owned by the caller. See Data contracts for the exact distinction.

Pane header

The fixed pane header owns the selected-date label and time-format preference. It remains above the independently scrolling pane content.

Selected date

Web appLocal state

Show the selected date using Cal's UI language. The header uses a short weekday and numeric day, with explicit font-semibold for Cal Sans. Its date follows the selected calendar day in the attendee's timezone, not the browser/server's local day. The subdued day number is separate from the weekday.

Selected date
Tue15
12h24h

Include the month when the selected day belongs to a different month from the displayed calendar, including across a year boundary. Cal currently adds a month for extra-day columns; the target also supports the calendar's next-month overflow days. Changing the displayed month alone must not silently relabel a retained selection.

Overflow day · October while viewing September
ThuOct 1
12h24h
Localized date · Italian
mar15
12h24h

Time format

Web appLocal state

The 12h/24h control changes displayed times throughout the Booker, including the event information and booking form. It must not change the selected instant, request new slots, or clear selection. Cal's shared time preference owns persistence; an isolated preview's preference is local to that example.

Use the Coss segmented-control pattern, through Cal's Segmented Radio Group. Time format is an explicit choice between two values, not an on/off preference or a set of tab panels.

Name the group “Time format” and expose both “12h (12-hour format)” and “24h (24-hour format).” Exactly one remains selected. Tab enters at the selected option; arrow keys move focus and selection, and Space selects the focused option without clearing it. Use the primitive's direction-aware keyboard behavior for RTL.

Time format
12h24h

Cal hides it when the day's first slot entry is an absence and in the separate mobile slot dialog, whose normal header is suppressed. The target also omits it during initial loading and when there is no selected date; it remains available during a slot refresh.

Reschedule controls

These controls occupy the fixed area between the pane header and scrollable content. Their authorization and event conditions are independent; when both render, Host preference comes first.

Host preference

Web appAPI v2

Host preference chooses the assignment policy for a rescheduled round-robin booking; it never chooses a named host. “Keep the same host” pins the booking to its original host. “Any available host” lets round-robin select from the eligible team and can expose more times.

Offer this choice only when a rescheduleUid is present, the event uses round-robin scheduling, allowRRHostChoiceOnReschedule is enabled, and routed team-member parameters are not controlling host assignment.

Mandatory initial choice

When the eligible flow has no stored rescheduleWithSameHost value, automatically open a modal before loading availability. Use a controlled Coss Dialog, not an AlertDialog: this is required input rather than a destructive confirmation. Disable pointer dismissal, reject Escape and outside-close requests, omit close and Cancel controls, and close only when Continue records the radio-group choice. “Keep the same host” is selected initially.

Compose each option as Coss Field → FieldLabel → Radio, using the hover and checked-card treatment from p-radio-group-4. The whole card must label and activate its radio.

The launcher below exists only so the mandatory state can be inspected without trapping the documentation page. It is not a production trigger.

Documentation control only. In the Booker, this dialog opens automatically before availability loads.

Change preference

After the initial choice, keep the same Select available so the person rescheduling can revise it. Reuse the exact control documented in Booker Event Information, including its icon, size="sm" trigger, option labels and descriptions, and alignItemWithTrigger={false} popup.

In the ordinary Booker, the compact Select belongs in event information. Move that same component above the time slots only when event details are hidden or when the separate mobile slot dialog owns the slot controls. Only its width changes: the right-panel placement fills the available row, while the left-panel placement stays content-sized.

Place it after the date header and before Show unavailable times, when both controls are eligible. Changing the preference clears stale time selection and loads availability for the new host context. The preview demonstrates clearing selection but does not calculate a new host schedule.

Host preference · hidden event details
Tue15
12h24h

Show unavailable times

Web appAPI v2 · Partial

Use Coss's Switch Field composition so the visible label names and activates the switch. This host-reschedule control and its slot status dots are the canonical composition.

Show it only to a signed-in caller rescheduling a booking, after viewer.bookings.isHostOfBooking confirms they are a host of that booking. Place it above the scrollable content, after Host preference when that control is also eligible.

Enabling it does not reveal a generic list of disabled times. Cal requests a second, authenticated schedule that overrides the rescheduling host's calendar conflicts, Cal bookings, working hours, out-of-office periods, and per-host limits. For a sole effective host, event booking windows, minimum notice, and restriction schedules can also be bypassed. Other hosts' constraints remain enforced for multi-host events.

Cal compares the override response with the normal response. While bypass is active, every non-seated row gets a status dot: use Coss's bg-success token when that start is also normally available and bg-destructive when it exists only in the override response. bypassed therefore means override-only, not necessarily “host busy”; the suppressed reason may instead be working hours, out of office, or a limit. Include “Normally available” or “Available only with host override” in the button's accessible name rather than relying on color alone. Seated rows keep their seat status instead.

The current Cal setter preserves the selected instant when the switch changes. The preview mirrors that behavior rather than inventing a reset; an implementation must still revalidate a retained selection against the active response before confirmation. Turning the switch off removes override-only rows.

Show unavailable times · authorized host rescheduling
Tue15
12h24h

Never infer bypass authorization from a URL flag or from the presence of a switch. A caller's ability to read public slots is not permission to override a host's availability.

Time-slot button

Every offered instant uses the same button component. Seat information, calendar or host-override status, repeated-time disambiguation, selection, and inline confirmation are variants of that button—not separate slot-list components.

Base and seated states

Web appAPI v2

Render the server-offered starts for the selected date in chronological order, in one column. Buttons use bg-muted, rounded-lg, medium-weight tabular numbers, and the same primary ring on hover and keyboard focus. Their small extended hit area must not cover the adjacent confirmation button or remain active on disabled slots.

Ordinary activation proceeds to the booking flow. A selected outline persists for tentative inline confirmation and controlled multi-selection consumers. Cal does not have a generic “select a slot, then click Next” step for ordinary month-view booking.

Available, full, and current seated booking
Tue15
12h24h

Choose a time to inspect selection.

For seated events, an offered start without bookingUid creates a new booking; a start with a different bookingUid joins the booking selected by the server. Disable the start when its bookingUid matches the seated booking being rescheduled. Cal currently leaves that reason implicit and continues to show seat availability; the target replaces that non-actionable count with a text-success-foreground check icon and neutral “Current booking” text. Keep the icon aria-hidden because the text carries the meaning. A full seated start says “No more seats available” and is also disabled.

Also disable offered starts while a booking, recurring booking, instant booking, or email-verification submission is pending. In skip-form flows, a required CAPTCHA token gates activation. Do not fill the rail with invented disabled working-hour intervals: ordinary unavailable starts are normally omitted by the server. Full seated rows, a stale selection, the current seated booking, and authorized host-bypass rows are different cases. Host-bypass busy rows stay actionable when authorized.

Repeated local times

During the autumn clock change, two different instants can both display as 02:30. Keep the original ISO start as the identity, React key, and selection value; format it only for display. Detect collisions among the slots for the same rendered date after applying the active locale and 12/24-hour format. Add a derived UTC±HH:MM offset only to colliding labels so they remain visibly and accessibly distinct. Both buttons below remain independently selectable.

Europe/Rome · October 25 · two distinct 02:30 starts
Sun25
12h24h

Choose a time to inspect selection.

Changing timezone must regroup the original instants by their date in the new zone, then detect label collisions again. Never reconstruct a booking instant from a formatted wall-time label.

Seat availability

Web appAPI v2

When seatsPerTimeSlot is enabled, show a second line inside the button. seatsShowAvailabilityCount selects one presentation for the entire event: an exact remaining count or qualitative availability. Never mix the two modes in one time list. A new seated start can have no existing booking UID or attendee count; treat it as having zero booked seats.

Exact availability

Use the normalized remaining-seat count. The web-app source derives it by subtracting the slot's booked attendee count from seatsPerTimeSlot; API v2 supplies it as seatsRemaining. Use the localized singular or plural label, and do not add a qualitative badge.

Exact remaining seat count
Tue15
12h24h

Choose a time to inspect selection.

Qualitative availability

With exact counts hidden, occupancy below 50% means “Seats available,” at least 50% adds a secondary “Filling fast” badge, and at least 83% adds a warning “Almost full” badge. Keep the availability text and badge together on the second line, matching the event-information composition.

Reserve status dots in the time rail for binary availability signals such as host override availability and attendee calendar conflicts. Reusing green, yellow, or red dots for seat occupancy would make the same visual language mean different things across otherwise identical rows. Text labels and tokenized badges name the gradual occupancy state directly, remain understandable without color, and leave exact counts visually quiet.

Qualitative seat availability
Tue15
12h24h

Choose a time to inspect selection.

Handle full before either presentation: disable the slot and replace its availability metadata with “No more seats available.” Preserve the slot's booked count and bookingUid through selection; the server owns occupancy and round-robin seat attribution. Do not combine the counts of different host bookings in the client.

Calendar conflict status

Web appAPI v2 · PartialLocal state

The connected-calendar overlay marks whether a host-offered time is available for the attendee. It does not change the host's availability or make an overlapping time unbookable. Show a success status dot for an available time and a destructive status dot for an overlapping time. Include “Available in your calendar” or “Busy in your calendar” in the corresponding button's accessible name so color is not the only programmatic signal.

Attendee calendar availability · select an overlapping time, then inspect Confirm
Tue15
12h24h

Make the entire overlapping time-slot button the tooltip trigger; the dot is not a separate interactive target. Hovering or focusing the slot reveals “Busy in your calendar” and the conflicting interval before selection, while keeping the rail compact in languages with longer translations. Set disableHoverablePopup so moving into the non-interactive tooltip does not keep it open. The tooltip supplements the button's accessible name rather than carrying the status by itself.

Selecting an overlapping time reveals a tentative inline Confirm action instead of advancing immediately. Selecting it again clears the tentative choice; choosing another overlapping time moves the action. A non-overlapping time follows the ordinary booking flow immediately.

Only show these indicators while the attendee has enabled the overlay and authorized access to the calendar data. Authorized host-reschedule indicators take precedence over calendar indicators, and seated rows use their seat-availability treatment instead. Never expose the attendee's calendar event title in a slot response or the conflict UI.

Pane messages

The scrollable content area renders the slot list or one of the replacement messages below. Partial-day absence is the exception: its message is ordered before or after the real slot buttons.

Out of office and holidays

Web appAPI v2 · Partial

Absence entries are not time slots. If every entry is an absence, Cal replaces the buttons with an out-of-office or holiday notice. A partial-day notice appears before or after the real buttons according to the returned boundary entry. Preserve that order; never normalize an absence timestamp into a bookable button.

All-day absence · public note and alternative host
Tue15

Render the emoji as the custom icon in the default Status Mark. Only display note text when showNotePublicly is true. Wrap that user-authored note in typographic double quotes, break long words, and constrain it to an independently scrolling region. Place it directly below the out-of-office title with no gap. A private or empty note has no text block. Keep the replacement-host explanation separate from the note so Cal-owned guidance is never mistaken for host-authored content.

Show the replacement-host explanation when both display names are available. The redirect additionally requires the target's id and a resolved organization-aware username or username. It preserves the selected date, month, and layout. The examples only report this handoff.

Partial-day absence · notice before afternoon slots
Tue15

When the first entry is an absence, render its notice before the real slots and hide the time-format control. When the last entry is an absence, render its notice after the real slots and retain the time-format control because the first entry is bookable.

Partial-day absence · notice after morning slots
Tue15
12h24h
Holiday · reason without a host or redirect
Tue15

The holiday form requires a reason but no host. Use that reason as the title, place “No availability on this holiday” directly below it with no gap, and omit the public-note, replacement-host, and redirect branches.

The Coss target uses the notice's emoji, title, public note, replacement-host explanation, and optional redirect with Coss tokens, scrolling, and buttons. API v2's public slot output filters out absence entries; this cannot be implemented from that response alone. Suppress these notices when the slot list is rendered in platform/settings-preview mode.

Loading, empty, and error

Web appAPI v2Local state

Use the request-state selector to inspect each branch. “Resolve demo request” deliberately completes a simulated request; it is documentation tooling outside the dashed pane boundary, not a production control or a latency measurement.

Initial loading, refresh, empty states, error, and recovery
Demo request state
Loading available times

Synthetic availability. No booking is created.

Keep every availability state inline in the time-slot pane. Do not auto-advance the calendar or open a no-availability dialog. The calendar remains visible so the attendee retains context while the rail explains the result and owns recovery.

“View first availability” promises a destination already present in loaded data. “Find first available time” starts a search. For a finite booking window, search until either the first eligible date is found or the complete remaining window has been evaluated. For an unbounded booking window, apply an explicit search horizon and report the range checked instead of claiming there is no future availability. Only the complete evaluation of a finite booking window permits the final no-availability state.

Evaluate zero slots before testing whether every returned slot is an absence. An empty list is an availability empty state; only a non-empty list made entirely of absence entries is an all-day absence.

Conditions and state priority

Resolve pane-level conditions in this order. Once a replacement state matches, do not continue into a lower-priority branch. Eligible reschedule controls remain in the fixed area unless the row explicitly says the initial choice blocks availability.

Within an offered row, seated-event treatment takes precedence over host-reschedule status; authorized host-reschedule status takes precedence over attendee-calendar status. Pending or stale-selection disabling applies after the visual variant is resolved.

Selection workflows

Web appAPI v2 · PartialLocal state

The time-slot UI selects an offered instant; the Booker owner decides what happens next. These examples stop at the handoff and allow returning to the list. They do not duplicate the booking form, CAPTCHA, payment, or verification implementations.

Ordinary booking

Activation selects the slot, preserves its seat metadata, and opens the details/form step. The normal month layout replaces the calendar and time rail with the form; a configured booking-form dialog uses the same selection. Returning clears the time and tentative confirmation, restores the date, and refreshes availability as needed.

Ordinary selection · immediate handoff, then back
Tue15
12h24h

Prefilled booking

When all required responses validate, Cal may skip the separate form. Multiple locations and attendee-input locations prevent this shortcut. The first click reveals the inline action; clicking the same tentative time hides it, and choosing another moves it. Only the explicit action submits.

Valid prefill · tentative selection and Confirm
Tue15
12h24h

Email verification and payment

The action can say “Verify email” until the required verification is satisfied. Otherwise use Cal's booking CTA resolver: online payment says “Pay to book,” offline payment says “Pay at location,” and events without a resolved positive payment price say “Confirm.” None of these labels permits bypassing form validity, CAPTCHA, verification, or payment handling.

Verification required
Tue15
12h24h
Paid event
Tue15
12h24h

Pending and no longer available

While submission or verification is pending, disable competing slots and show Coss Button's loading state on the relevant action. A stale selection is not a server-returned slot variant: a schedule refresh can remove it from the list entirely. If Cal's quick availability check retains the selected row in an inline-confirm flow, disable both the row and its action and label that action “Taken.” In the booking form, show the full unavailable-slot alert, disable submission, and direct the attendee back to choose another time. A booking conflict is not a successful booking and must not leave an enabled retry against the stale start.

Submission pending
Tue15
12h24h
Dormant quick-check state · E2E only
Tue15
12h24h

Do not treat unavailableTimeSlots as a generic list of disabled schedule rows. Production can remove a stale start during schedule refresh or reject it during booking; the booking form then shows the full unavailable-slot alert and returns the attendee to time selection. Preserve that recovery contract when migrating the Booker without assuming this dormant sibling-button treatment.

Email Embed multiple selection

The Email Embed editor is the shipped multi-selection consumer of these slot buttons. It lets an organizer toggle several offered starts, including starts on different dates, before copying the resulting email content. Keep every chosen instant selected while navigating dates, mark calendar dates that contain selections, and clear the collection when a duration change invalidates it. This is authoring state: clicking a selected start removes it and does not open the booking form. Match Cal's current composition: the date header is followed directly by the buttons, with selection communicated by the button treatment rather than additional instructional or count text.

Email Embed · select or remove several offered times
Tue15
12h24h

Booking proposals · API contract only

Booking proposals are a different capability. An event type may require an attendee to submit from one up to bookingProposalCount distinct candidate starts, after which a host confirms one candidate and creates the booking. API v2 supports this contract, but the public web Booker does not currently consume bookingProposalCount or create proposals. Do not treat Email Embed selection as proposal UI, and do not invent the attendee proposal composition until that flow has a product-owned call site. Proposals do not support reschedules, seated events, recurring events, or paid events; if any candidate becomes unavailable, the complete proposal submission fails.

Complete examples

Use the layout selector above to inspect the same rail at mobile, compact, and wide sizes. Long lists scroll independently while the date and format control stay in place. These examples inspect selection; the progression examples above demonstrate leaving the pane.

Ordinary event · scrollable time list
Demo request state
Tue15
12h24h

Synthetic availability. No booking is created.

Seated event
Demo request state
Tue15
12h24h

Synthetic availability. No booking is created.

Mobile and keyboard behavior

In ordinary mobile month view, the time rail sits below the calendar. Cal scrolls to it once after date selection unless embed disableAutoScroll is enabled. Match that behavior without repeatedly moving the page on format changes or refreshes.

Cal also supports slotsViewOnSmallScreen: selecting a date opens a separate full-screen slot dialog. Its header contains Back, the weekday/full date, duration, and timezone; the ordinary date/format header is hidden. The host preference moves into this surface when eligible. Back returns to calendar selection, and normal slot selection closes the slot dialog before opening the form. This optional wrapper is a Cal-specific integration requirement outside the inline mobile rail and these isolated previews. Reuse the event-information controls when composing it; respect locked timezone state.

Slots are native buttons: Tab visits enabled starts in order, Enter/Space activates, and disabled buttons cannot submit. They are not a calendar grid and do not need roving arrow-key navigation. Keep visible focus rings unclipped by scrolling and scroll a focused row into view. The format selector and reschedule controls precede slots in focus order. An inserted confirmation follows its time in that order; move focus appropriately when the owner replaces the pane, and restore it on return.

Use logical start/end borders and padding for RTL. Localize dates, times, CTA text, and seat plurals in production; hardcoded English and fixed dates belong only to these demos. Formatters should use the attendee's timezone explicitly, and the selected calendar date should remain a date key rather than an implicitly parsed local timestamp.

Data contracts

The app-local owner remains apps/web/modules/bookings; it supplies normalized data and callbacks to the target presentation. This documentation does not move those components into a new SDK or introduce a shared transport adapter.

Keep the query context intact: event identity, attendee timezone, selected duration, booking window, routing/eligible hosts, reschedule UID, host preference, and authorized bypass. A response for an old context must not overwrite the current rail. Date, duration, timezone, and host changes must revalidate selection; stale tentative confirmation cannot survive simply because its time label still appears.

Performance and data loading

Reuse the calendar's query/cache and group slots once per date; the rail should not independently fetch the same range. Share in-flight requests. Prefetch a bounded upcoming range without delaying visible slots, respecting the booking window and team-event cost. Measure the chosen range and next-range prefetch strategy rather than fixing one window for every Cal event.

Preserve Cal's focus/interval refresh, reservation renewal/checks, release on selection changes/unmount, and conflict recovery. A cached month must not remain authoritative forever. Distinguish refreshing the same context from loading a different one; never enable a stale slot during a context change. Reserving a slot is advisory, and the booking mutation still validates availability.

Format timestamps at the display boundary, reuse a formatter per locale/timezone/format, and index unavailable starts instead of repeatedly scanning a large collection. Do not fetch or recompute slot availability when only switching 12h/24h. Recheck past starts and minimum notice as time passes; client filtering is not a replacement for server validation.

Before production migration, compare cold load, cached navigation, request counts, and server availability cost for individual, seated, collective, and large round-robin events. These local examples demonstrate presentation and transitions, not measured production performance. Behavioral tests belong with the migrated owner/component, not a separate calendar or scheduling engine in this docs app.

Implementation references

This page defines the target contract. The sources below are implementation references for the conditions and composition; reconcile them to this blueprint when either Booker changes.

Example URLs

Not exhaustive—these Cal paths show the time-slot area within the complete booking flow. Event configuration and authenticated context determine which conditional parts appear.