Multi-service booking composer. Customers assemble their own wellness day by adding services across categories (massage/facial/body/nails/add-ons), with live package summary, duration totals, animated bundle-discount progress bar, quick-start presets, and remove-from-package controls.
A multi-service booking composer that renders a category-tabbed catalog of services (each with an icon, name, description, duration, and price) above a live "Your package" panel, where each pick gets a quantity stepper, a running subtotal and total-duration tally, an animated bundle-discount progress bar that applies a percentage off once a minimum item count is reached, per-line remove controls, and a final book button.
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/booking22?email=YOUR_EMAIL&license_key=YOUR_KEY"Base UI flavor
npx shadcn add "https://ui.beste.co/r-base/booking22?email=YOUR_EMAIL&license_key=YOUR_KEY"This installs the block to components/beste/block/booking22.tsx, plus the badge, button, and tabs shadcn/ui primitives it uses for the eyebrow badge, the add/book buttons, and the category filter tabs.
The installed file exports booking22Demo alongside the block: the exact props behind the preview above. Spread it to get a working package builder in one line.
import { Booking22, booking22Demo } from "@/components/beste/block/booking22";
export default function Page() {
return <Booking22 {...booking22Demo} />;
}Then replace the demo with your own props. Written out, a trimmed setup looks like this:
import { Hand, Heart, Sparkles } from "lucide-react";
import { Booking22 } from "@/components/beste/block/booking22";
export default function Page() {
return (
<Booking22
badge={{ label: "Build your day", variant: "secondary" }}
heading="Design your <strong>spa day</strong>"
description="Mix and match across categories, bundle three or more and save."
currency="$"
categories={[
{ value: "massage", label: "Massage", icon: Hand },
{ value: "facial", label: "Facial", icon: Heart },
]}
services={[
{
value: "swedish",
category: "massage",
name: "Swedish Massage",
description: "Gentle full-body massage to release tension",
duration: 60,
price: 89,
icon: Hand,
},
{
value: "aromatherapy",
category: "facial",
name: "Aromatherapy",
description: "Essential oil upgrade for any service",
duration: 0,
price: 25,
icon: Sparkles,
maxQty: 5,
},
]}
bundleDiscount={{
minItems: 3,
percent: 15,
progressTemplate: "Add {remaining} more to save {percent}%",
activeTemplate: "Bundle discount applied · −{percent}%",
}}
/>
);
}| Prop | Type | Default | Description |
|---|---|---|---|
badge | { label: string; variant?: "default" | "secondary" | "outline" } | – | Optional eyebrow badge above the heading |
heading | string | – | Section heading, supports inline <strong> |
description | string | – | Paragraph rendered under the heading |
currency | string | "$" | Symbol prefixed to every price and total |
categories | PackageCategory[] | [] | Tabs that filter the service list |
services | PackageService[] | [] | All bookable services, shown by active category |
bundleDiscount | BundleDiscountConfig | – | Optional minimum-items discount and progress bar |
labels | Booking22Labels | {} | Overrides for every piece of UI text |
className | string | – | Extra classes for the outer <section> |
type PackageCategory = {
value: string;
label: string;
icon: LucideIcon;
};
type PackageService = {
value: string;
category: string;
name: string;
description: string;
duration: number; // minutes; 0 hides the duration line
price: number;
icon: LucideIcon;
maxQty?: number; // caps the stepper; defaults to 10
};
type BundleDiscountConfig = {
minItems: number;
percent: number;
progressTemplate: string; // supports {remaining} and {percent}
activeTemplate: string; // supports {percent}
};
type Booking22Labels = {
addButton?: string;
addedLabel?: string;
packageHeading?: string;
emptyPackage?: string;
emptyHint?: string;
subtotalLabel?: string;
discountLabel?: string;
totalLabel?: string;
totalDurationLabel?: string;
bookButton?: string;
clearButton?: string;
singleServiceSuffix?: string;
multipleServicesSuffix?: string;
};useState: activeCategory (initialized to categories[0]?.value) and a counts record keyed by service.value. Nothing is lifted out, so the selected package is not observable via props or callbacks.s.category === activeCategory; services whose category does not match a tab value are never shown, and there is no "all" tab.Button (count 0) and a stepper (count > 0); the stepper + is disabled at maxQty (default 10), and pressing − at count 1 removes the item entirely. The package-panel per-line button also removes the whole line in one click via removeAll.bundleDiscount drives the progress bar: bundleActive flips true once totalItems >= minItems, discountAmount is Math.round(subtotal * percent / 100), and the discount row only renders when discountAmount > 0. progressTemplate/activeTemplate are plain string replacements of {remaining} and {percent}.heading is injected with dangerouslySetInnerHTML, so inline <strong> is styled bold/primary; pass only trusted markup.duration: 0 hides its clock line entirely and contributes nothing to the total-time tally (formatDuration renders "0m" only for the aggregate total when empty).bookButton early-returns when totalItems === 0 and otherwise only calls console.log("Package booking:", ...); wiring the actual booking submission is left to the integrator. Note that the addedLabel label exists in the Booking22Labels type but is never destructured or rendered, and the "quick-start presets" mentioned in the block description are not implemented in this code.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.
booking3
Upcoming and past appointments list grouped by date, with service name, provider avatar, time, duration, status badges (confirmed/pending/cancelled), and reschedule/cancel action buttons.
booking21
Gym or studio weekly schedule with day tabs, class list per day (instructor avatar, duration, spots remaining, difficulty), and quick reserve action. Tap a day to see its classes, tap a class to book a spot.
booking14
Tours and experiences booking card with hero image overlay, rating, duration, difficulty badge, date picker, traveler counter, language selector, highlights list, and reserve CTA. Perfect for guided tours, classes, and local experiences.
booking16
Residential cleaning service booking with service type tabs (standard/deep/move-out), bedroom/bathroom selectors, recurring frequency pills, add-ons checklist, date picker, estimated duration, and price breakdown footer.
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.