Booking Components

A compound family of booking primitives — BookingCalendar and BookingCalendarLegend — for lessons that explain stay-length pricing, blackout dates, range selection, or visualising per-night yield. Pass a slots[] array of { date, available, price? } and the calendar mirrors your inventory.

Preview
June 2026
$ Low$$ Mid$$$ PeakBlocked

Selected: 2026-06-082026-06-14

Customize
Display

Installation

npx shadcn@latest add https://craftbits.dev/r/booking-components.json

Usage

import { BookingCalendar } from "@craft-bits/core";
 
<BookingCalendar
  slots={[
    { date: "2026-06-01", available: true, price: 140 },
    { date: "2026-06-02", available: true, price: 160 },
    { date: "2026-06-03", available: false },
  ]}
  defaultValue={{ checkIn: "2026-06-01", checkOut: null }}
  defaultMonth={{ year: 2026, month: 5 }}
  onSelect={(range) => console.log(range)}
/>

Compose the optional legend alongside the calendar:

import { BookingCalendar, BookingCalendarLegend } from "@craft-bits/core";
 
<BookingCalendar slots={slots} showLegend />

Anatomy

  • BookingCalendar. A <div role="group"> with month-navigation controls, a presentation-role day-of-week strip, and a role="grid" of gridcell buttons. Each cell exposes data-cb-price-tier, data-cb-blocked, data-cb-selected, data-cb-checkin, and data-cb-checkout for styling hooks.
  • BookingCalendarLegend. A 4-swatch row mapping low | mid | high | blocked to plain-English bands. Render inline via showLegend on the calendar, or import the part directly when you need it elsewhere.

Understanding the component

  1. Inventory-driven. The consumer owns slots; the calendar never invents days, prices, or availability. Days outside the visible month are filtered out — slots can span multiple months and the navigator picks the right window.
  2. Range selection. First tap sets checkIn and clears checkOut. A later tap that's after checkIn becomes checkOut. A tap on or before checkIn resets the range to start there. Blocked days never trigger a selection.
  3. Controlled and uncontrolled. Pass value + onSelect for controlled, or defaultValue for uncontrolled. Same pattern for month + onMonthChange versus defaultMonth.
  4. Price tiers. When a slot has a price, the calendar derives a tier (low, mid, high) by comparing it to the visible month's min/max. The tier lands on data-cb-price-tier so designers can recolor cells without touching JSX.
  5. Reduced motion. Cell enter, layout shifts, and exit are all springs in SPRINGS.snap; under prefers-reduced-motion they short-circuit to instant.

Props

BookingCalendar

PropTypeDefaultDescription
slotsBookingSlot[]requiredInventory across one or more months.
valueBookingRange | nullControlled selection.
defaultValueBookingRange | nullnullUncontrolled initial selection.
onSelect(range) => voidFires when the user picks a date.
month{ year, month }Controlled visible month.
defaultMonth{ year, month }first slot or todayUncontrolled initial month.
onMonthChange({ year, month }) => voidFires when the user navigates.
currencystring"$"Symbol prefixed to prices.
hidePricesbooleanfalseHide per-day prices.
showLegendbooleanfalseRender the tier legend below the grid.
dayNamesstring[]EnglishLocalized weekday headings, Sunday-first.
monthNamesstring[]EnglishLocalized month names (length 12).
ariaLabelstring"Date picker"Region label.
prevMonthLabelstring"Previous month"Previous-button label.
nextMonthLabelstring"Next month"Next-button label.
classNamestringMerged onto the root via cn().

BookingCalendarLegend

PropTypeDefaultDescription
currencystring"$"Symbol used in default swatch labels.
labelsPartial<Record<BookingPriceTier, ReactNode>>Per-tier label overrides.

BookingSlot

FieldTypeDescription
datestringISO-8601 date (YYYY-MM-DD).
availablebooleanWhen false, the cell renders disabled.
pricenumberOptional per-night price.

BookingRange

FieldTypeDescription
checkInstringRange start.
checkOutstring | nullRange end, or null while a range is in progress.

Accessibility

  • The root is a <div role="group" aria-labelledby> so the visible month caption labels the region.
  • The day grid uses role="grid" and each cell is a gridcell button; blocked cells are disabled and visually struck-through.
  • Each cell has a verbose aria-label that reads the month, day, price, blocked/available status, and check-in / check-out marker so screen-reader users get the same context as sighted users.
  • Focus styling uses a 2px cb-accent ring via focus-visible.
  • Enter/exit and layout animations short-circuit under prefers-reduced-motion.

Credits

  • Extracted from: terminal-dreams (src/components/frontend-design/sdp-booking-platform/ui/BookingComponents.tsx). The original bundle fused three pieces (MiniCalendar, MiniMap, AvailabilityCalendar) to a useBooking context, a generateMonthDays engine, a 2026-only fixed scenario, a CSS-Module stylesheet, and the booking lab's view-mode router. craft-bits keeps the highest-value primitive — the per-night calendar with range selection and tier-keyed pricing — and drops the engine coupling, the scenario lock, the map, and the lab chrome. Surface now: slots[] = { date, available, price? } with controlled + uncontrolled selection and month navigation, an opt-in legend, and full localisation hooks.