- Accordion
- Avatar
- Badge
- Breadcrumb
- Button
- Calendar
- Checkbox
- Combobox
- Container
- CurrencyInput
- DistributionSlider
- Drawer
- Dropdown
- Grid
- Heading
- Image
- Input
- InputGroup
- Label
- Logo
- MapPin
- Modal
- NativeSelect
- NumberInput
- OtpInput
- PhoneInput
- Popover
- Progress
- PropertyCalendar
- RadioGroup
- RadioGroupCards
- ResponsiveModal
- ScrollArea
- SearchBar
- SearchBarFallback
- SearchInput
- Select
- Separator
- Spinner
- Switch
- Tabs
- Text
- Textarea
- Toast
- Toggle
- ToggleGroup
- Tooltip
BookingPanel
A contained booking card with date and guest selection, pricing display, and a reserve CTA.
pnpm add @wandercom/design-system-web
The BookingPanel block provides a self-contained booking interface with check-in/check-out date inputs, a guest selector, dynamic pricing header, and a submit button. It adapts its header and CTA label based on whether dates are selected.
When dateRange and onDateChange are provided, the PropertyCalendar is composed directly into the date inputs as a popover (desktop) or drawer (mobile bar). Clicking the date inputs opens the calendar automatically.
import {
BookingPanel,
BookingPanelMobileBar,
BookingPanelRoot,
formatGuestLabel,
} from '@wandercom/design-system-web/blocks/booking-panel';
import type { AvailabilityDay, DateRange } from '@wandercom/design-system-web';
import { format } from 'date-fns';
import { useState } from 'react';
export function Example() {
const [dateRange, setDateRange] = useState<DateRange>({ start: null, end: null });
const [guests, setGuests] = useState<number | null>(null);
const [pets, setPets] = useState<number | null>(null);
return (
<BookingPanelRoot
desktop={
<BookingPanel
dateRange={dateRange}
onDateChange={setDateRange}
availability={availabilityData}
stayRequirements={{ minNights: 3 }}
checkinDate={dateRange.start ? format(dateRange.start, "MM/dd/yyyy") : null}
checkoutDate={dateRange.end ? format(dateRange.end, "MM/dd/yyyy") : null}
guests={guests}
onGuestsChange={setGuests}
pets={pets}
onPetsChange={setPets}
petsAllowed
guestLabel={formatGuestLabel(guests, pets) || "1 guest"}
pricing={dateRange.start && dateRange.end ? { totalPrice: "$12,500", nightCount: 5 } : null}
onSubmit={() => handleReserve()}
/>
}
mobile={
<BookingPanelMobileBar
dateRange={dateRange}
onDateChange={setDateRange}
availability={availabilityData}
stayRequirements={{ minNights: 3 }}
checkinDate={dateRange.start ? format(dateRange.start, "MM/dd/yyyy") : null}
checkoutDate={dateRange.end ? format(dateRange.end, "MM/dd/yyyy") : null}
nightlyRate="$2,500"
onPress={handlePress}
/>
}
/>
);
}The panel adapts based on the props provided:
No dates selected -- The header displays "Select dates for pricing" and the CTA reads "Add dates". When calendar props are provided, clicking the CTA opens the calendar popover.
Dates selected with pricing -- The header shows the total price and night count, and the CTA reads "Reserve".
Pricing loading -- When pricingLoading is true, the header shows skeleton shimmer placeholders.
Request to book -- When bookingMode="request", the panel heading changes to "Request to book", a "Contact concierge to book this home." note appears above the CTA, and the button defaults to "Book live with us". The submitLabel prop still overrides the button label.
Error -- When error is set, the date input borders turn destructive red and the message is displayed between the inputs and the CTA.
Submitting -- When submitting is true, the CTA button shows a loading spinner.
BookingPanelRoot is a responsive container that switches between desktop and mobile content at the 744px breakpoint using useIsDesktop(). Pass separate desktop and mobile render props to show the appropriate layout per viewport. If either prop is omitted, children is used as the fallback.
import {
BookingPanel,
BookingPanelMobileBar,
BookingPanelRoot,
} from '@wandercom/design-system-web/blocks/booking-panel';
<BookingPanelRoot
desktop={<BookingPanel {...desktopProps} />}
mobile={<BookingPanelMobileBar {...mobileProps} />}
/>This is the recommended pattern for property detail pages where the desktop sidebar panel and the mobile fixed bottom bar share the same booking state.
The BookingPanel supports two patterns for guest management.
Integrated popover -- When guests and onGuestsChange are provided, the guests input becomes a Popover with NumberInput rows for guests and pets. Use formatGuestLabel to derive the display string from the current values.
import { BookingPanel, formatGuestLabel } from '@wandercom/design-system-web/blocks/booking-panel';
const [guests, setGuests] = useState<number | null>(null);
const [pets, setPets] = useState<number | null>(null);
<BookingPanel
guests={guests}
onGuestsChange={setGuests}
pets={pets}
onPetsChange={setPets}
petsAllowed
maxOccupancy={12}
maxPets={3}
guestLabel={formatGuestLabel(guests, pets) || "1 guest"}
// ...other props
/>When petsAllowed is false, the pets row is rendered but disabled with a value of 0.
External callback -- When guests/onGuestsChange are not provided, the panel falls back to the onGuestsClick callback so the host application can manage the guest picker externally.
<BookingPanel
guestLabel="3 guests, 1 pet"
onGuestsClick={() => openGuestPicker()}
// ...other props
/>Pass dateRange, onDateChange, and optionally availability, stayRequirements, rules, calendarAlert, pricePerNight, and onDatesSelected to compose the PropertyCalendar directly into the panel. The calendar opens as a Popover on desktop when date inputs or the "Add dates" CTA are clicked.
Use onDatesSelected to trigger a pricing API request once the user commits both dates — it fires after both check-in and check-out are set.
<BookingPanel
dateRange={dateRange}
onDateChange={setDateRange}
availability={availability}
stayRequirements={{ minNights: 3, checkInDays: ['Saturday'] }}
checkinDate={dateRange.start ? format(dateRange.start, "MM/dd/yyyy") : null}
checkoutDate={dateRange.end ? format(dateRange.end, "MM/dd/yyyy") : null}
pricePerNight="$787.86/night pre-tax"
onDatesSelected={(range) => fetchPricing(range)}
onSubmit={handleReserve}
/>BookingPanelMobileBar is a fixed bottom bar for mobile viewports. It supports the same calendar integration props and opens a vertically-scrolling 12-month PropertyCalendarMobile drawer when tapped.
import { BookingPanelMobileBar } from '@wandercom/design-system-web/blocks/booking-panel';
<BookingPanelMobileBar
dateRange={dateRange}
onDateChange={setDateRange}
availability={availability}
stayRequirements={{ minNights: 3 }}
checkinDate={dateRange.start ? format(dateRange.start, "MM/dd/yyyy") : null}
checkoutDate={dateRange.end ? format(dateRange.end, "MM/dd/yyyy") : null}
nightlyRate="$2,500"
onPress={handlePress}
/>When rating is provided with stars > 4 and no dates are selected, the star rating replaces the nightly rate in the subtitle. Resize to mobile viewport to see the bar.
Use the children prop to inject additional content between the guest selector and the CTA, such as a coupon input or price breakdown.
<BookingPanel
dateRange={dateRange}
onDateChange={setDateRange}
checkinDate={dateRange.start ? format(dateRange.start, "MM/dd/yyyy") : null}
checkoutDate={dateRange.end ? format(dateRange.end, "MM/dd/yyyy") : null}
pricing={{ totalPrice: "$12,500", nightCount: 5 }}
onSubmit={() => handleReserve()}
>
<PriceBreakdown items={lineItems} />
</BookingPanel>desktop?:
mobile?:
children?:
className?:
dateRange?:
onDateChange?:
availability?:
availabilityLoading?:
rules?:
stayRequirements?:
calendarAlert?:
pricePerNight?:
onDatesSelected?:
checkinDate?:
checkoutDate?:
guests?:
onGuestsChange?:
pets?:
onPetsChange?:
petsAllowed?:
maxOccupancy?:
maxPets?:
guestLabel?:
pricing?:
pricingLoading?:
error?:
submitLabel?:
submitDisabled?:
submitting?:
disclaimer?:
bookingMode?:
onCheckinClick?:
onCheckoutClick?:
onGuestsClick?:
onSubmit?:
children?:
className?:
dateRange?:
onDateChange?:
availability?:
availabilityLoading?:
rules?:
stayRequirements?:
calendarAlert?:
checkinDate?:
checkoutDate?:
pricing?:
nightlyRate?:
rating?:
onPress?:
submitLabel?:
submitting?:
bookingMode?:
className?:
dateRange?:
onDateChange?:
availability?:
availabilityLoading?:
rules?:
stayRequirements?:
calendarAlert?:
onDatesSelected?:
checkinDate?:
checkoutDate?:
guests?:
onGuestsChange?:
pets?:
onPetsChange?:
petsAllowed?:
maxOccupancy?:
maxPets?:
pricing?:
pricingLoading?:
guestLabel?:
pricePerNight?:
submitLabel?:
submitDisabled?:
submitting?:
disclaimer?:
error?:
bookingMode?:
onSubmit?:
onPress?:
nightlyRate?:
rating?:
children?:
className?:
formatGuestLabel
function formatGuestLabel(
guests: number | null | undefined,
pets: number | null | undefined
): stringReturns a formatted string like "3 guests, 1 pet" or "2 guests". Returns an empty string when both values are null/undefined. Handles singular/plural forms automatically.
The shared calendar integration interface extended by both BookingPanel and BookingPanelMobileBar.
interface BookingPanelCalendarProps {
dateRange?: DateRange;
onDateChange?: (range: DateRange) => void;
availability?: AvailabilityDay[];
availabilityLoading?: boolean;
rules?: BookingRule[];
stayRequirements?: StayRequirements;
calendarAlert?: ReactNode;
onDatesSelected?: (range: DateRange) => void;
}The guest management interface extended by BookingPanel.
interface BookingPanelGuestsProps {
guests?: number | null;
onGuestsChange?: (value: number | null) => void;
pets?: number | null;
onPetsChange?: (value: number | null) => void;
petsAllowed?: boolean;
maxOccupancy?: number; // default 16
maxPets?: number; // default 5
}type BookingPanelPricing = {
totalPrice: string;
nightCount: number;
};type BookingPanelRating = {
stars: number;
reviewCount: number;
};