Minimal Service Picker

Book a visit

Choose what you need, then a time

Prices are published and cancellations more than a day ahead are free.

Soft blue gradient backdrop

This week

May
M12
T13
W14
T15
F16
S17
S18

Six slots open on Wednesday, from 08:00 to 16:30.

Initial consultation

A full hour with a clinician, ending in a written plan you take away.

Length
60 min
Price
£180
Next available
Wednesday 14 May
Choose a time

Covered by an insurer? The price shown is what they are billed.

About this block

Minimal Service PickerPRO

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.

Booking26: Minimal Service Picker

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.

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/booking26?email=YOUR_EMAIL&license_key=YOUR_KEY"

Base UI flavor

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

Quick start

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.

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

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

Props

PropTypeDefaultDescription
badge{ label: string }–Eyebrow above the hairline rule, rendered through Badge23
headingstring–Section heading in the left column of the header
descriptionstring–Supporting paragraph, right-aligned from md up
servicesService[][]Selectable services, the first one starts selected
labelsDetailLabels–The three row labels in the detail table
mediaReactNode–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
footnotestring–Small line under the action
classNamestring–Extra classes for the outer section
ts
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;
};

Behavior notes

  • The list rows deliberately show only name, length and price. Everything else about a service, its summary and next opening, appears in the panel once it is selected, which is what keeps a six-service list scannable in one glance.
  • Selection is one piece of local useState holding an index, starting at 0. There is no onChange prop and no controlled mode.
  • Selected rows are marked by a filled bg-muted surface plus a weight and colour change on the name, never by colour alone.
  • Rows carry px-4, so the selected fill extends past the text rather than cutting tight against it.
  • The tile and the detail table share one 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.
  • Prices use tabular-nums in both the list and the table, so the column stays optically aligned across different digit counts.
  • The rows are real button elements with aria-pressed and explicit cursor-pointer, since Tailwind v4 buttons default to cursor: default.

More Booking blocks

View all Booking
PRO

booking24

Service And Slot Picker

PRO

booking33

Session Picker

PRO

booking18

Cinema Seat Picker

Get Pro access