PropertyCalendar

A booking-aware date range picker that extends Calendar with per-day availability states, stay requirements, and booking rules.

Installation

pnpm add @wandercom/design-system-web

Usage

18 lines
import { PropertyCalendar } from '@wandercom/design-system-web/ui/property-calendar';
import type { AvailabilityDay, DateRange } from '@wandercom/design-system-web/ui/property-calendar';
import { useState } from 'react';

export function Example() {
  const [value, setValue] = useState<DateRange>({ start: null, end: null });

  return (
    <PropertyCalendar
      value={value}
      onChange={setValue}
      availability={availabilityData}
      stayRequirements={{ minNights: 3, checkInDays: ['Saturday'] }}
      onClose={() => setOpen(false)}
      onClearDates={() => setValue({ start: null, end: null })}
    />
  );
}

Examples

Loading example...

Checkout-only days (isBlocked: true, canCheckOut: true, status: "booked")

Days at the end of a booking where the guest checks out have canCheckOut: true and isBlocked: true simultaneously. These should render as restricted with a "Check-out only" tooltip, not as unavailable with a strikethrough. Hover the last day of each booked block below to confirm.

Loading example...

Day states

Each calendar day is evaluated against availability data and the current selection state. Days resolve to one of four visual states:

available -- The day is selectable. Renders as a standard interactive calendar day with text-primary.

restricted -- The day is bookable but constrained for the current selection context. Rendered with text-secondary at full opacity, disabled (not clickable), and shows a tooltip explaining the constraint. Restriction tooltips have two distinct activation contexts:

In browse mode (value.start === null, no dates selected), check-in restriction labels appear immediately on hover:

  • Days where check-in is restricted (canCheckIn: false) -- shows "Check-out only" if the date is reachable as a checkout (both canCheckOut: true and present in the precomputed validCheckoutDates set), otherwise "Check-in unavailable"
  • Days with no reachable checkout from this date (validCheckinDates miss) -- same tooltip logic as above
  • Days that don't match checkInDays in stayRequirements -- shows "Check-out only" if reachable as a checkout, otherwise "Check-in unavailable"

When a complete range is selected (value.start !== null && value.end !== null), check-in restriction tooltips continue to display on hover, matching browse mode behavior.

In checkout selection mode (value.start !== null && value.end === null), checkout restriction tooltips are deferred: they do not appear immediately when a check-in date is clicked. They only activate after the user moves the cursor into the calendar section for the first time following check-in selection. Once that first mouse movement occurs, these restrictions appear on hover:

  • Days where check-out is restricted (canCheckOut: false) -- shows "Check-out unavailable"
  • Days that don't match checkOutDays in stayRequirements -- shows "Check-out unavailable"
  • Days that violate min/max night constraints -- shows constraint tooltip (e.g. "Min 3 nights", "Max 7 nights")

inactive -- The day is structurally disabled. Rendered with muted text, no strikethrough, not clickable. Used for:

  • Past dates (before today)
  • Dates before a selected check-in (when selecting check-out)
  • Days blocked by booking rules such as same-day-turnover, gap-nights, advance-booking, or quota-exhausted -- these also show a tooltip with the rule message

unavailable -- The day is booked, blocked, or under maintenance. Rendered with reduced opacity and a diagonal strikethrough. No tooltip — the strikethrough is sufficient. Applies to dates with status: "blocked", status: "maintenance", or isBlocked: true (when status !== "booked"). status: "booked" days are exceptions: those with canCheckIn: true render as available in browse mode. Those with canCheckOut: true render as restricted ("Check-out only") in browse mode only if they are reachable — meaning the backward scan from that date finds a valid check-in path within stay constraints, placing the date in the precomputed validCheckoutDates set. Interior booked days that happen to have canCheckOut: true but have no reachable check-in path (surrounded by other booked days) are excluded from that set and continue to render as strikethrough unavailable. When a check-in is selected, any booked+canCheckOut day where the stay interior is unobstructed (no blocked day between check-in and checkout) remains selectable as a checkout endpoint via validateCheckoutDate.

Variants

PropertyCalendar

The base calendar content component, used standalone or composed into wrappers. Its header exposes the date inputs when its own container reaches 65rem (1040px), rather than depending on viewport width.

PropertyCalendarDesktop

Wraps PropertyCalendar in a Popover with a paginated 2-month view and prev/next navigation arrows. Used by BookingPanel for its wide container layout.

Loading example...

PropertyCalendarMobile

Wraps PropertyCalendar in a Drawer with 12 months rendered vertically in a scrollable view. Navigation arrows are hidden; users scroll through months instead. Used by BookingPanelMobileBar for its narrow container layout.

Loading example...

With pricing

When both dates are selected and pricePerNight is provided, the calendar header shows the nightly rate.

Loading example...

Pricing loading

When pricingLoading is true, a skeleton shimmer replaces the price per night in the header.

Loading example...

Error

When booking rules are violated, the calendar displays an alert with the relevant error message.

Loading example...

Types

DateRange

4 lines
interface DateRange {
  start: Date | null;
  end: Date | null;
}

AvailabilityDay

Per-day availability data keyed by date string in YYYY-MM-DD format.

10 lines
interface AvailabilityDay {
  date: string;
  canCheckIn: boolean;   // when true and status === "booked", overrides isBlocked for check-in validation
  canCheckOut: boolean;  // when true and status === "booked", overrides isBlocked for checkout validation
  isBlocked?: boolean;
  minNights?: number;
  maxNights?: number;
  nightPrice?: number;
  status: 'available' | 'booked' | 'maintenance' | 'blocked';
}

BookingRule

A booking rule that may restrict date selection. The message field is intended for display to the user.

12 lines
interface BookingRule {
  type:
    | 'min-nights'
    | 'max-nights'
    | 'check-in-day'
    | 'check-out-day'
    | 'advance-booking'
    | 'gap-nights'
    | 'same-day-turnover'
    | 'quota-exhausted';
  message: string;
}

StayRequirements

Stay requirement constraints displayed as an info banner between the header and calendar.

6 lines
interface StayRequirements {
  minNights?: number;
  maxNights?: number;
  checkInDays?: string[];
  checkOutDays?: string[];
}

Props

PropertyCalendar

value

DateRange
Current date range selection with nullable start and end.

onChange

(range: DateRange) => void
Called when the date range changes.

availability?:

AvailabilityDay[]
Per-day availability data. Each entry is keyed by YYYY-MM-DD date string.

availabilityLoading?:

boolean
Whether availability data is currently loading.

rules?:

BookingRule[]
Booking rules that may restrict date selection.

stayRequirements?:

StayRequirements
Stay requirement constraints displayed as an info banner (e.g. min nights, check-in days).

numberOfMonths?:

number
Number of months to display. Defaults to 2.

hideNavigation?:

boolean
Hides prev/next month navigation arrows and forces vertical (flex-col) month layout. Used internally by PropertyCalendarMobile for the scrollable experience.

hideFooter?:

boolean
Hides the footer row with Close and Clear dates actions.

hideClose?:

boolean
Hides only the Close button within the footer.

hideWeekdays?:

boolean
Hides the weekday header row rendered per month by react-day-picker.

fullWidth?:

boolean
Removes horizontal padding and makes the calendar stretch full width.

weekStartsOn?:

0 | 1
First day of the week. 0 = Sunday, 1 = Monday. Defaults to 0.

minDate?:

Date
Earliest selectable date. Defaults to today.

maxDate?:

Date
Latest selectable date. Defaults to 24 months from today.

alert?:

ReactNode
Alert content rendered between the header and calendar.

info?:

ReactNode
Info banner content rendered below the alert.

pricePerNight?:

string
Pre-formatted price per night string, e.g. "$787.86/night pre-tax". Shown in the header when both dates are selected.

pricingLoading?:

boolean
When true, shows a skeleton shimmer in place of the price per night.

onDatesSelected?:

(range: DateRange) => void
Called when both check-in and check-out dates are committed. Use this to trigger a pricing API request.

className?:

string
Additional CSS classes for the root element.

onClose?:

() => void
Called when the close button in the footer is clicked.

onClearDates?:

() => void
Called when the clear dates button in the footer is clicked.

onError?:

() => void
Called when availability data reveals the current date range overlaps unavailable days. Fires at most once per unique start/end pair. Use to clear or reset the range.

error?:

boolean
When true, marks both date inputs with destructive border styling.

hideCheckInDaysBanner?:

boolean
Hides the check-in days info banner even when stayRequirements.checkInDays is set.

PropertyCalendarContent

Low-level content renderer exported from @wandercom/design-system-web/ui/property-calendar/shared and used by the inline, desktop, and mobile variants. Accepts all PropertyCalendar props plus:

desktopLayout?:

boolean
When true, shows the desktop header inputs and forces a horizontal multi-month layout without waiting for the container breakpoint.

onSelectComplete?:

() => void
Called after the user commits a complete date range.

hideHeader?:

boolean
When true, hides the calendar header.

PropertyCalendarHeader

Low-level header renderer exported from @wandercom/design-system-web/ui/property-calendar/shared. Its desktopLayout prop is primarily used by PropertyCalendarContent:

desktopLayout?:

boolean
When true, shows the desktop date inputs without waiting for the header container breakpoint.

PropertyCalendarDesktop

Extends a subset of PropertyCalendar props plus:

open?:

boolean
Controls the popover open state.

onOpenChange?:

(open: boolean) => void
Called when the popover open state changes.

trigger?:

ReactNode
Trigger element that opens the popover.

side?:

'bottom' | 'left' | 'right' | 'top'
Popover placement side. Defaults to "bottom".

sideOffset?:

number
Offset from the trigger in pixels. Defaults to 8.

align?:

'start' | 'center' | 'end'
Alignment along the side. Defaults to "start".

alignOffset?:

number
Offset along the alignment axis in pixels.

numberOfMonths?:

1 | 2
Number of months to display. Defaults to 2.

info?:

ReactNode
Info banner content rendered below the alert.

pricePerNight?:

string
Pre-formatted price per night string, e.g. "$787.86/night pre-tax". Shown in the header when both dates are selected.

pricingLoading?:

boolean
When true, shows a skeleton shimmer in place of the price per night.

onDatesSelected?:

(range: DateRange) => void
Called when both check-in and check-out dates are committed. Use this to trigger a pricing API request.

onClose?:

() => void
Called when the calendar is explicitly closed via the Close button. Runs before the popover closes.

onError?:

() => void
Called when availability data reveals the current date range overlaps unavailable days. Fires at most once per unique start/end pair.

error?:

boolean
When true, marks both date inputs with destructive border styling.

PropertyCalendarMobile

Extends a subset of PropertyCalendar props plus:

open?:

boolean
Controls the drawer open state.

onOpenChange?:

(open: boolean) => void
Called when the drawer open state changes.

trigger?:

ReactNode
Trigger element that opens the drawer.

title?:

string
Heading text shown at the top of the drawer. Defaults to "Select dates".

description?:

string
Description text shown below the heading. Defaults to "Add your check-in & check-out dates.". Pass an empty string to hide.

saveLabel?:

string
Label for the save/submit button. Defaults to "Save".

saveDisabled?:

boolean
Disables the save button. Defaults to false.

info?:

ReactNode
Info banner content rendered below the alert.

onClose?:

() => void
Called when the calendar is explicitly closed via the Save button. Runs before the drawer closes.

onDatesSelected?:

(range: DateRange) => void
Called when both check-in and check-out dates are committed. Use this to trigger a pricing API request.

onError?:

() => void
Called when availability data reveals the current date range overlaps unavailable days. Fires at most once per unique start/end pair.

error?:

boolean
When true, marks both date inputs with destructive border styling.

Accessibility

PropertyCalendar inherits keyboard navigation from the underlying Calendar component (react-day-picker).

Unavailable days show a diagonal strikethrough with no tooltip. Booked days with canCheckIn: true render as available. Booked days with canCheckOut: true lose the strikethrough and render as restricted ("Check-out only") only when they are reachable — the backward scan must find a valid check-in path within stay constraints, placing the date in validCheckoutDates. Interior booked days with canCheckOut: true that have no such path remain strikethrough unavailable. Restricted days show contextual tooltips explaining the constraint (e.g. "Check-in unavailable", "Min 3 nights") while remaining visible at full opacity. In browse mode (no dates selected), check-in restriction tooltips appear on hover immediately. In checkout selection mode, checkout restriction tooltips are deferred until the user first moves the cursor into the calendar section after selecting a check-in date. When a complete date range is selected, no restriction tooltips are shown. Inactive days (past, maintenance, blocked, and certain booking-rule violations) render muted; those triggered by booking rules also show a tooltip with the rule message. The check-in and check-out input fields include aria-label attributes for screen reader support.

PropertyCalendar