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
SuMoTuWeThFrSa
$ Low$$ Mid$$$ PeakBlocked
Selected: 2026-06-08 → 2026-06-14
Customize
Display
Installation
npx shadcn@latest add https://craftbits.dev/r/booking-components.jsonUsage
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, apresentation-role day-of-week strip, and arole="grid"ofgridcellbuttons. Each cell exposesdata-cb-price-tier,data-cb-blocked,data-cb-selected,data-cb-checkin, anddata-cb-checkoutfor styling hooks. - BookingCalendarLegend. A 4-swatch row mapping
low | mid | high | blockedto plain-English bands. Render inline viashowLegendon the calendar, or import the part directly when you need it elsewhere.
Understanding the component
- 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. - Range selection. First tap sets
checkInand clearscheckOut. A later tap that's aftercheckInbecomescheckOut. A tap on or beforecheckInresets the range to start there. Blocked days never trigger a selection. - Controlled and uncontrolled. Pass
value+onSelectfor controlled, ordefaultValuefor uncontrolled. Same pattern formonth+onMonthChangeversusdefaultMonth. - 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 ondata-cb-price-tierso designers can recolor cells without touching JSX. - Reduced motion. Cell enter, layout shifts, and exit are all springs in
SPRINGS.snap; underprefers-reduced-motionthey short-circuit to instant.
Props
BookingCalendar
| Prop | Type | Default | Description |
|---|---|---|---|
slots | BookingSlot[] | required | Inventory across one or more months. |
value | BookingRange | null | — | Controlled selection. |
defaultValue | BookingRange | null | null | Uncontrolled initial selection. |
onSelect | (range) => void | — | Fires when the user picks a date. |
month | { year, month } | — | Controlled visible month. |
defaultMonth | { year, month } | first slot or today | Uncontrolled initial month. |
onMonthChange | ({ year, month }) => void | — | Fires when the user navigates. |
currency | string | "$" | Symbol prefixed to prices. |
hidePrices | boolean | false | Hide per-day prices. |
showLegend | boolean | false | Render the tier legend below the grid. |
dayNames | string[] | English | Localized weekday headings, Sunday-first. |
monthNames | string[] | English | Localized month names (length 12). |
ariaLabel | string | "Date picker" | Region label. |
prevMonthLabel | string | "Previous month" | Previous-button label. |
nextMonthLabel | string | "Next month" | Next-button label. |
className | string | — | Merged onto the root via cn(). |
BookingCalendarLegend
| Prop | Type | Default | Description |
|---|---|---|---|
currency | string | "$" | Symbol used in default swatch labels. |
labels | Partial<Record<BookingPriceTier, ReactNode>> | — | Per-tier label overrides. |
BookingSlot
| Field | Type | Description |
|---|---|---|
date | string | ISO-8601 date (YYYY-MM-DD). |
available | boolean | When false, the cell renders disabled. |
price | number | Optional per-night price. |
BookingRange
| Field | Type | Description |
|---|---|---|
checkIn | string | Range start. |
checkOut | string | null | Range 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 agridcellbutton; blocked cells aredisabledand visually struck-through. - Each cell has a verbose
aria-labelthat 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-accentring viafocus-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 auseBookingcontext, agenerateMonthDaysengine, 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.