Book a session
Tell us a little about what brings you here and when you are usually free. We reply within a working day, always in person, never with a form letter.
Three practitioners, one of whom will read this personally.
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.
A session request built as a conversation rather than a checkout: contact lines, practitioner avatars and a reassurance on one side, and on the other a form where session type and preferred time are picked as pills instead of dropdowns.
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/booking30?email=YOUR_EMAIL&license_key=YOUR_KEY"Base UI flavor
npx shadcn add "https://ui.beste.co/r-base/booking30?email=YOUR_EMAIL&license_key=YOUR_KEY"This installs the block to components/beste/block/booking30.tsx, the shadcn input, label, and textarea primitives the form is built on, and the button22 sliding-marker pill and the text1 word stagger it uses.
The installed file exports booking30Demo alongside the block: the exact props behind the preview above. Spread it to get a working booking form in one line.
import { Booking30, booking30Demo } from "@/components/beste/block/booking30";
export default function Page() {
return <Booking30 {...booking30Demo} />;
}Then replace the demo with your own props. Written out, a trimmed setup looks like this:
import { Booking30 } from "@/components/beste/block/booking30";
export default function Page() {
return (
<Booking30
eyebrow="Book a session"
heading="Choose a time that feels right. We will take it from there."
description="Tell us a little about what brings you here and when you are usually free."
contacts={[
{ label: "Email", value: "hello@beste.co" },
{ label: "Hours", value: "Monday to Friday, 9 to 18" },
]}
avatars={[{ src: "/team/one.jpg", alt: "Portrait of a practitioner" }]}
proof="Three practitioners, one of whom will read this personally."
sessionTypes={["First conversation", "Individual therapy", "Not sure yet"]}
timeSlots={["Mornings", "Afternoons", "Evenings"]}
labels={{
name: "Your name",
email: "Email",
sessionType: "What are you looking for?",
time: "When are you usually free?",
message: "Anything you would like us to know",
submit: "Send the request",
note: "Nothing is booked until we have spoken.",
}}
/>
);
}| Prop | Type | Default | Description |
|---|---|---|---|
eyebrow | string | – | Small-caps line above the heading |
heading | string | – | Section heading, rendered as an h2 through Text1 |
description | string | – | Intro paragraph under the heading |
contacts | ContactLine[] | [] | Ruled contact rows in the left column |
avatars | Avatar[] | [] | Overlapping practitioner portraits |
proof | string | – | Reassurance printed beside the avatars |
sessionTypes | string[] | [] | Pill choices for the session type; the group is skipped when empty |
timeSlots | string[] | [] | Pill choices for preferred time |
labels | Partial<FormLabels> | {} | Every string in the form |
className | string | – | Extra classes for the outer <section> |
type ContactLine = {
label: string;
value: string;
};
type Avatar = {
src: string;
alt: string;
};
type FormLabels = {
name: string;
email: string;
sessionType: string;
time: string;
message: string;
submit: string;
note: string;
};aria-pressed, not a select or a radio group. They are single-choice toggles, so the pressed state is announced without inventing a listbox a keyboard user would then have to navigate.useId, so two of these on a page keep their labels pointing at their own inputs.dl on a three-column grid with the value spanning two, so long addresses wrap under themselves instead of pushing the label out of alignment.-space-x-3 and each carries a border-background ring, which is what separates them from one another rather than a gap.preventDefault() on submit; nothing is sent anywhere until you wire it up.Wiring the form up
This block ships the form markup only; state, validation, and submit are yours to add. Our guide wires the shadcn Field primitives to React Hook Form, TanStack Form, and Formisch on one field system.
booking33
A booking chooser: a serif heading that settles in word by word and an intro, then a ruled list of session types on the left that swaps, with a soft blur, the muted detail card on the right showing a serif title, a description, a fact row and a sliding-marker pill.
booking31
A visit-us section: a photograph drifting inside its frame on the left, and on the right a serif heading that settles in word by word, an intro, a ruled sheet with the serif address, directions and opening hours, a sliding-marker pill and a text link.
booking32
The classic contact section: a serif heading that settles in word by word, an intro and a ruled contact sheet on the left, and a plain form with name, email and message fields and a sliding-marker pill on the right, with no panel around it.
booking25
A confirmation screen with an accent eyebrow, a two-column hairline table of the appointment details, a checked list of what to bring, and a sticky tile floating the live phone view the member also receives.