A quiet booking picker whose left column lists each service as a single hairline row carrying only its length and price, swapping a sticky panel that floats a live week strip above the chosen service's detail and its book action.
A quiet booking picker whose left column lists each service as a single hairline row carrying only its length and price, swapping a sticky panel that floats a live week strip above the chosen service's detail and its book action.
Upgrade to Pro
Pro blocks install through the shadcn CLI with your license key and ship their full source. Docs and live previews stay open to everyone, so you can read every block's details first.
Swap YOUR_EMAIL and YOUR_KEY for the email and license key on your account. Find your license key on your account page.
Radix flavor
npx shadcn add "https://ui.beste.co/r/booking26?email=YOUR_EMAIL&license_key=YOUR_KEY"Base UI flavor
npx shadcn add "https://ui.beste.co/r-base/booking26?email=YOUR_EMAIL&license_key=YOUR_KEY"This installs the block to components/beste/block/booking26.tsx, the calendar32 week-strip piece it floats on the tile (installed to components/beste/piece/calendar32.tsx), and the badge23 and button21 components it uses for the eyebrow and the book action.
The installed file exports booking26Demo alongside the block: the exact props behind the preview above. Spread it to get a working picker in one line.
import { Booking26, booking26Demo } from "@/components/beste/block/booking26";
export default function BookingPage() {
return <Booking26 {...booking26Demo} />;
}Then replace the demo with your own props. Written out, a trimmed setup looks like this:
import { Booking26 } from "@/components/beste/block/booking26";
import { Calendar32 } from "@/components/beste/piece/calendar32";
export default function BookingPage() {
return (
<Booking26
badge={{ label: "Book a visit" }}
heading="Choose what you need, then a time"
description="Prices are published and cancellations more than a day ahead are free."
services={[
{
name: "Initial consultation",
summary: "A full hour with a clinician, ending in a written plan.",
duration: "60 min",
price: "£180",
next: "Wednesday 14 May",
},
{
name: "Review appointment",
summary: "Checking an existing plan and adjusting what is not working.",
duration: "30 min",
price: "£95",
next: "Tomorrow, 09:00",
},
]}
labels={{ duration: "Length", price: "Price", next: "Next available" }}
media={
<Calendar32
title="This week"
month="May"
days={[{ weekday: "W", date: "14", slots: 6 }]}
selectedIndex={0}
caption="Six slots open on Wednesday."
/>
}
image={{ src: "/backdrops/blue.jpg", alt: "Soft blue gradient backdrop" }}
button={{ label: "Choose a time", href: "/booking/times" }}
footnote="Covered by an insurer? The price shown is what they are billed."
/>
);
}| Prop | Type | Default | Description |
|---|---|---|---|
badge | { label: string } | – | Eyebrow above the hairline rule, rendered through Badge23 |
heading | string | – | Section heading in the left column of the header |
description | string | – | Supporting paragraph, right-aligned from md up |
services | Service[] | [] | Selectable services, the first one starts selected |
labels | DetailLabels | – | The three row labels in the detail table |
media | ReactNode | – | Live asset on the tile, calendar32 in the demo |
image | { src: string; alt: string } | – | Backdrop behind the media tile |
button | { label: string; href: string } | – | Book action under the detail table |
footnote | string | – | Small line under the action |
className | string | – | Extra classes for the outer section |
type ActionLink = {
label: string;
href: string;
};
type Service = {
name: string;
summary: string;
duration: string;
price: string;
next: string;
};
type DetailLabels = {
duration: string;
price: string;
next: string;
};useState holding an index, starting at 0. There is no onChange prop and no controlled mode.bg-muted surface plus a weight and colour change on the name, never by colour alone.px-4, so the selected fill extends past the text rather than cutting tight against it.lg:sticky lg:top-24 lg:self-start column, so the whole panel travels together while the service list scrolls. The self-start is required for sticky to work inside a grid row.labels drives all three detail rows, and the block reads the values straight off the selected service, so the table cannot drift out of sync with the list.tabular-nums in both the list and the table, so the column stays optically aligned across different digit counts.button elements with aria-pressed and explicit cursor-pointer, since Tailwind v4 buttons default to cursor: default.booking24
An interactive appointment picker: a hairline panel pairing a selectable service list with a scrollable day strip and a time slot grid that marks unavailable times, beside a full-height provider photo with a gradient name card, closing on a live summary bar and confirm action.
booking1
Split-panel booking interface with interactive monthly calendar on the left and time slot grid on the right. Features today highlight, disabled dates, selected state, morning/afternoon grouping, and booking summary footer.
booking4
Service selection grid with category filter tabs, service cards showing icon, name, description, duration, price, and selected state. Ideal for spa, salon, clinic, and fitness booking flows.
booking18
Interactive seat selection grid for cinemas, theaters, and events. Row/column layout with an aisle gap, tier-based pricing (standard/premium/VIP), occupied seats, a live selection summary, and a legend. Includes screen indicator at the top.