Spa Day Package Builder

Build your day

Design your spa day

Pick as many treatments as you like. Mix and match across categories — bundle three or more and save 15% automatically.

Swedish Massage

Gentle full-body massage to release tension

1h$89

Deep Tissue

Targeted pressure on chronic muscle knots

1h$120

Hot Stone Therapy

Heated basalt stones for deep relaxation

1h 15m$140

Your package

Your package is empty

Pick treatments above to build your day

About this block

Spa Day Package BuilderPRO

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.

Booking22: Spa Day Package Builder

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.

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.

Upgrade Now

Installation

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

bash
npx shadcn add "https://ui.beste.co/r/booking22?email=YOUR_EMAIL&license_key=YOUR_KEY"

Base UI flavor

bash
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.

Quick start

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.

tsx
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:

tsx
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}%",
      }}
    />
  );
}

Props

PropTypeDefaultDescription
badge{ label: string; variant?: "default" | "secondary" | "outline" }Optional eyebrow badge above the heading
headingstringSection heading, supports inline <strong>
descriptionstringParagraph rendered under the heading
currencystring"$"Symbol prefixed to every price and total
categoriesPackageCategory[][]Tabs that filter the service list
servicesPackageService[][]All bookable services, shown by active category
bundleDiscountBundleDiscountConfigOptional minimum-items discount and progress bar
labelsBooking22Labels{}Overrides for every piece of UI text
classNamestringExtra classes for the outer <section>
ts
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;
};

Behavior notes

  • All state is internal 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.
  • The service list is filtered strictly by s.category === activeCategory; services whose category does not match a tab value are never shown, and there is no "all" tab.
  • Each row toggles between an outline "Add" 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.
  • A service with 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).
  • The 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.

More Booking blocks

View all Booking
FREE

booking4

Service Selector

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.

PRO

booking24

Service And Slot Picker

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.

PRO

booking30

Session Request

A booking section: a serif heading that settles in word by word over the intro, a ruled contact sheet and a practitioner avatar line, beside a calm request form with name and email fields, pill choices for session type and preferred time, a message box and a sliding-marker pill to send.

PRO

booking3

Appointment List

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.

PRO

booking19

Multi-step Booking Wizard

Stepper-based booking flow with numbered progress dots, animated progress bar, and four steps: service selection, schedule, contact details, and review. Back/Next navigation, per-step validation, and a live summary.

PRO

booking21

Weekly Class Schedule

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.