Appointment Booking Page React template for Tailwind CSS v4
v1.0.0Added in 1.14.0
A React appointment booking template for clinics, salons and coaches: next free times, a booking widget, services, team, hours and a map.
A one-page site for a service business built around booking. The hero shows whether the practice is open right now (with when it closes or opens next, in the business time zone) and the next free appointments for the first service; picking one glides down to the booking widget with that time and service filled in, on the details step. Book an appointment, the header Book button and See all times glide to the widget. The widget is the full booking flow: service and practitioner, a calendar with the free times (closed days disabled, times in the practice's zone with a switch), the details form and a confirmation with an .ics download. Every service row has its length and price and a Book button that opens the widget on the times for that service; every practitioner card lists specialties and languages, and Book a visit preselects that person. Opening hours mark today and the Open now status; on phones days with the same hours share a row. Insurance and payment notes, an FAQ accordion with a contact card, and the address, phone, email and directions next to a map (below the contact card on phones). On phones a Book bar with the next free time slides up from the bottom while the hero, the widget and the footer are out of view. Keyboard: every slot, Book button and link is reachable with Tab and activates with Enter or Space; the widget keeps its own arrow-key navigation. Reduced motion keeps everything still.
Appointment Booking Page is a Pro React template in the React website templates category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add appointment-booking-page, together with Booking Widget, FAQ Section, Feature Grid, Marketing Section, Marketing Header and 11 more and the npm packages lucide-react and motion.
Built with
The sections this page is made of. In the full-screen preview, hold Alt to outline each one on the page.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add appointment-booking-pageAppointment Booking Page is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import AppointmentBookingPage from @/components/templates/appointment-booking-page.
// app/page.tsx
"use client"
import Link from "next/link"
import type { Booking, BookingSlotContext } from "@/components/blocks/booking-widget"
import { AppointmentBookingPage } from "@/components/templates/appointment-booking-page"
async function getTimeSlots(day: Date, { service, staff, timeZone }: BookingSlotContext) {
const params = new URLSearchParams({ day: day.toDateString(), service: service.id, staff: staff?.id ?? "any", timeZone })
const res = await fetch(`/api/availability?${params}`)
return (await res.json()) as string[]
}
async function book(booking: Booking) {
await fetch("/api/bookings", { method: "POST", body: JSON.stringify(booking) })
}
export default function HomeRoute() {
return <AppointmentBookingPage linkComponent={Link} getTimeSlots={getTimeSlots} onBook={book} />
}Props
businessdefault"APPOINTMENT_BUSINESS"AppointmentBusiness{ name, tagline?, address, city, phone, email?, timeZone, hours, location?, directionsHref?, rating?, reviewCount?, hoursNote? }. hours is a list of { day (0 Sunday to 6 Saturday), open, close } wall times in timeZone; a day without an entry is closed, two entries make a lunch break. location { lat, lng } places the map pin.
servicesdefault"APPOINTMENT_SERVICES"AppointmentService[]BookingService plus category and priceFrom: { id, name, duration (minutes), price?, description?, category?, priceFrom?, disabled? }. A price of 0 shows Free; priceFrom shows from $180.
categoriesdefault"APPOINTMENT_SERVICE_CATEGORIES"AppointmentServiceCategory[]The service groups: { value, label, description? }. Services without a known category go under More services.
staffdefault"APPOINTMENT_STAFF"AppointmentStaff[]BookingStaff plus specialties, bio and languages: { id, name, role?, avatar?, services?, specialties?, bio?, languages?, disabled? }. services lists the ids a person offers (empty: all).
statsdefault"APPOINTMENT_STATS"AppointmentStat[]The numbers under the hero: { value, label }. [] hides them.
visitNotesdefault"APPOINTMENT_VISIT_NOTES"AppointmentVisitNote[]The Before your visit card next to the hours: { title, description }. [] hides the card.
paymentNotesdefault"APPOINTMENT_PAYMENT_NOTES"FeatureGridItem[]The insurance and payment cards: { icon, title, description }.
faqdefault"APPOINTMENT_FAQ"FaqItem[]The questions: { question, answer, category? }.
getTimeSlots(day: Date, context: BookingSlotContext) => (string | DateTimeSlot)[] | Promise<(string | DateTimeSlot)[]>The free times of a day as "HH:mm" wall times in context.timeZone, as in BookingWidget; it feeds both the widget and the next free times in the hero. Pass a stable function. Default: createHoursTimeSlots over business.hours, a few times taken per day.
slotStepdefault30numberMinutes between the default free times (without getTimeSlots).
onBook(booking: Booking) => void | Promise<unknown>Called by Confirm booking with { service, staff, start, end, timeZone, details, bookedAt }. A promise keeps the button loading; a rejection shows the error. Default: a pretend 900 ms request.
bookingWindowdefault60numberHow many days ahead can be booked. 0 means no limit.
nextSlotsdefault3numberHow many next free times the hero card lists (up to 6). 0 hides the card.
nextSlotServicedefault""stringThe service id the hero looks up free times for. Empty: the first service.
bookingLayoutdefault"split""split" | "stacked"The widget's layout: the summary column beside the steps, or a narrower card with the summary on top.
hourCycledefault"auto""auto" | "h12" | "h23"12 or 24 hour times in the hours, the free times and the widget; auto follows the locale.
currencydefault"USD"stringThe ISO currency of the prices.
localedefault""stringFormats days, times, prices and numbers. Empty: the UIProvider locale, else en-US.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The booking widget, the service and payment icon tiles and the open FAQ item.
ctaTonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The solid Book buttons in the header, the hero and the phone Book bar.
radiusdefault"lg""none" | "sm" | "md" | "lg"Corners of the cards and the widget: 0, 12, 16 or 24px.
backgrounddefault"glow""plain" | "soft" | "glow" | "aurora" | "grid" | "dots"The page's main background (PageBackdrop).
backgroundColordefault"oklch(0.62 0.14 275)"stringThe tint of the main background and its decoration.
bandBackgrounddefault"muted""muted" | "tinted" | "gradient" | "dots"The look of every second section (the booking band first).
bandColordefault"oklch(0.62 0.14 275)"stringThe tint of tinted and gradient bands; the backgroundColor when equal.
showServicesdefaulttruebooleanThe services grouped by category, each with its length, price and a Book button.
showTeamdefaulttruebooleanThe practitioner cards with specialties, languages and Book a visit.
showHoursdefaulttruebooleanThe opening hours with today marked, the Open now status and the Before your visit card.
showPaymentdefaulttruebooleanThe insurance and payment notes (a FeatureGrid).
showFaqdefaulttruebooleanThe FAQ (a FaqSection with a contact card).
showContactdefaulttruebooleanThe address, phone, email, status and directions.
showMapdefaulttruebooleanThe map next to the contact card (below it on phones). Needs business.location.
stickyBookdefaulttruebooleanPhones: a Book bar with the next free time pinned to the bottom while the hero, the widget and the footer are out of view.
headerdefaulttruebooleanThe sticky header with the section links, the phone number and Book.
footerdefaulttruebooleanThe footer with Care, Visit and Practice links.
headerPropsPartial<MarketingHeaderProps>Anything for MarketingHeader (layout, variant, announcement...); wins over the page's own values.
footerPropsPartial<MarketingFooterProps>Anything for MarketingFooter (variant, columns, socials, newsletter...).
bookingPropsPartial<BookingWidgetProps>Anything for the embedded BookingWidget (requirePhone, showNotes, timeZones, rescheduleHref, labels...).
paymentPropsPartial<FeatureGridProps>Anything for the insurance and payment FeatureGrid (layout, columns, card...).
faqPropsPartial<FaqSectionProps>Anything for the FaqSection (layout, searchable, jsonLd...).
nowDate | numberWhat now means for the open status, today and the free times. Default: the clock.
animateIndefaulttruebooleanThe hero rises in once when the page mounts on the client; never during hydration, under reduced motion or in thumbnails.
linkComponent(props: LinkComponentProps) => ReactNodeThe router link for the header, footer and FAQ links (next/link in an app).
labelsPartial<AppointmentBookingPageLabels>Every string of the page (DEFAULT_APPOINTMENT_BOOKING_PAGE_LABELS holds the English defaults), including the formatters for durations, prices, the open status and the free times.
classNamestringClasses for the root.
classNamesPartial<Record<AppointmentBookingPageSlot, string>>Classes for header, hero, nextSlots, booking, widget, services, service, team, member, hours, visit, payment, faq, contact, map, sticky and footer.
Parts
createHoursTimeSlots
functionBuilds a getTimeSlots function from opening hours: a free time every step minutes inside each opening, shown in the visitor's zone, with a few taken per day and person. The page's default; use it to prototype before your availability API exists.
hours*AppointmentHoursRange[]The opening hours.
timeZone*stringThe IANA zone the hours are in.
options{ step?: number; seed?: string }Minutes between times (default 30) and a seed for the taken ones.
getBusinessOpenStatus
functionOpen or closed at a moment, with the closing time or the next opening: { open, today, closesAt?, opensAt?, opensIn?, opensDay? }. Read in the business time zone.
hours*AppointmentHoursRange[]The opening hours.
timeZone*stringThe IANA zone the hours are in.
now*Date | numberThe moment to check.
APPOINTMENT_BOOKING_PAGE_SAMPLE
constantAll the sample content in one object.
APPOINTMENT_BUSINESS
constantBrightside Dental on South Lamar in Austin, open Monday to Saturday in Central Time.
APPOINTMENT_SERVICES
constantEight dental services from a new patient exam to a same-day crown, with lengths and prices.
APPOINTMENT_SERVICE_CATEGORIES
constantCheckups and cleanings, Cosmetic, Fillings and crowns, Urgent care.
APPOINTMENT_STAFF
constantThree dentists and a hygienist with specialties, bios, languages and the services they offer.
APPOINTMENT_STATS
constantThe three numbers under the hero.
APPOINTMENT_VISIT_NOTES
constantFour Before your visit notes: forms, cards, parking, access.
APPOINTMENT_PAYMENT_NOTES
constantSix insurance and payment cards with icons.
APPOINTMENT_FAQ
constantSix questions about new patients, insurance, cancellations, emergencies, nerves and payment plans.
DEFAULT_APPOINTMENT_BOOKING_PAGE_LABELS
constantThe English strings.
AppointmentBookingPageProps
typeProps of AppointmentBookingPage, including the attributes of the root element.
AppointmentBookingPageLabels
typeEvery string of the page.
AppointmentBookingPageRadius
type"none" | "sm" | "md" | "lg".
AppointmentBookingPageSlot
typeThe keys of classNames.
AppointmentBusiness
typeThe practice: name, address, phone, time zone, hours and map position.
AppointmentHoursRange
type{ day, open, close }: one opening of a weekday, as "HH:mm".
AppointmentWeekday
type0 (Sunday) to 6 (Saturday).
AppointmentService
typeA bookable service with its category.
AppointmentServiceCategory
type{ value, label, description? }.
AppointmentStaff
typeA practitioner with specialties, bio and languages.
AppointmentStat
type{ value, label }.
AppointmentVisitNote
type{ title, description }.
AppointmentSlot
typeA free start the hero lists: { start, day, time }.
AppointmentOpenStatus
typeWhat getBusinessOpenStatus returns.
Examples
A hair salon
The same page for a salon in New York: other business, hours (closed Mondays), services, categories and stylists, with brand Book buttons.
Brand accents
tone and ctaTone brand on an aurora page with tinted bands: the widget, icon tiles and Book buttons turn orange.
Closed right now
now is a Sunday evening: the status reads Closed now, Opens tomorrow at 8:00 AM, and the first free times are on Monday.
A lean booking page
Only the hero, the widget (stacked), the hours and the contact card, with square-ish corners and a dotted background.
A slow availability API
getTimeSlots answers after 1.2s: the hero card shows skeleton rows until the first free times arrive.
Dependencies
Files
- components/templates/appointment-booking-page.tsx
- components/templates/appointment-booking-page-data.ts
FAQ
Is the React Appointment Booking Page template free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Appointment Booking Page?
Run npx wingo-ui@latest add appointment-booking-page. It also adds the Wingo UI items it builds on (Booking Widget, FAQ Section, Feature Grid, Marketing Section, Marketing Header and 11 more) and the npm packages lucide-react and motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/appointment-booking-page.
How does Appointment Booking Page behave on phones?
Opening hours mark today and the Open now status; on phones days with the same hours share a row. Insurance and payment notes, an FAQ accordion with a contact card, and the address, phone, email and directions next to a map (below the contact card on phones).