Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Forms

Booking Widget

Forms

Booking Widget React block for Tailwind CSS v4

Address InputCSV Import
Live props
State
defaultService
defaultStaff
defaultDay
defaultStep
Behavior
anyStaff
startTime
endTime
minuteStep30 min
bookingWindow60 days
defaultTimeZone
requirePhone
defaultCountry
locale
Appearance
hourCycle
tone
color
radius
Layout
layout
Content
showPhone
showNotes
location
locationType
currency
icsFileName
Code
import BookingWidget from "@/components/blocks/booking-widget"

<BookingWidget />
  1. Home
  2. Components
  3. Forms
  4. Booking Widget
Pro

Booking Widget React block for Tailwind CSS v4

v1.0.0·Added in 1.14.0

A React booking widget: pick a service and a team member, a day and time, add your details and get a confirmation with an .ics file.

A Cal.com style booking flow in one card. A summary column on the left shows the host's avatar, the service, its length, the location, the price, your time zone and, once picked, the day and time range; it updates as you choose. The steps at the top (Service, Time, Details) show where you are, and completed steps are buttons that go back. Step 1: pick a service from soft cards with the price at the end, and optionally a team member (Anyone available first, then people who offer that service). Step 2: the inline date and time picker, with weekends, past days and days past the booking window disabled, the free times of the chosen day beside the calendar, and a time zone switch in its footer that keeps the moment and moves the wall clock. Step 3: full name, email, phone (optional unless requirePhone) and notes. Continue and Back are real buttons; Continue without a time, or with an empty name or a bad email, shows the error in place and focuses the first problem. Confirm booking spins while onBook runs, then the card shows You're booked with a recap, Add to calendar (downloads an .ics file), Reschedule (back to the times with your details kept, confirmed as New time confirmed) and Book another. Keyboard: Tab moves through the cards, calendar, times and fields; arrows move between service cards, days and times as in the date-time-picker; Enter on a time continues, Enter in a field submits the step; focus moves to each new step's heading. Below 768px the card stacks: the summary becomes a header row with the avatar, service, length and the picked time, the times drop under the calendar, and the Back / Continue bar sticks to the bottom of the screen above the safe area. Each step fades in; reduced motion keeps it still.

Booking Widget is a Pro React block in the React form components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add booking-widget, together with Avatar, Button, Date Time Picker, Input, Phone Input and 9 more and the npm packages motion and lucide-react.

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 booking-widget

Booking Widget 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 BookingWidget from @/components/blocks/booking-widget.

import { BookingWidget, type Booking, type BookingSlotContext } from "@/components/blocks/booking-widget"

const services = [
  { id: "cut", name: "Haircut", duration: 45, price: 55, location: "120 Mercer St, New York", locationType: "in-person" as const },
  { id: "color", name: "Color and gloss", duration: 90, price: 140, location: "120 Mercer St, New York", locationType: "in-person" as const },
]

const staff = [
  { id: "emma", name: "Emma Carter", role: "Senior stylist" },
  { id: "liam", name: "Liam Brooks", role: "Colorist", services: ["color"] },
]

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[]
}

export function BookAppointment() {
  const book = async (booking: Booking) => {
    await fetch("/api/bookings", { method: "POST", body: JSON.stringify(booking) })
  }
  return <BookingWidget services={services} staff={staff} getTimeSlots={getTimeSlots} onBook={book} location="Salon" locationType="in-person" />
}

Props

  • servicesdefault "DEFAULT_BOOKING_SERVICES"
    BookingService[]

    What can be booked: { id, name, duration (minutes), price?, description?, location?, locationType?, icon?, disabled? }. With one service and no staff the service step is skipped.

  • staffdefault "[]"
    BookingStaff[]

    Optional team members: { id, name, role?, avatar?, services? (ids they offer), disabled? }. Adds a who choice under the services; people who do not offer the picked service are hidden.

  • hostdefault "DEFAULT_BOOKING_HOST"
    BookingHost

    The person the summary shows while no team member is picked: { name, role?, avatar? }.

  • defaultServicedefault ""
    string

    The service id picked at first. Empty: the first enabled service.

  • defaultStaffdefault ""
    string

    The team member id picked at first. Empty: "any" when anyStaff is on.

  • anyStaffdefault true
    boolean

    Offers Anyone available as the first team choice; getTimeSlots then gets staff: null.

  • getTimeSlots
    (day: Date, context: BookingSlotContext) => (string | DateTimeSlot)[] | Promise<(string | DateTimeSlot)[]>

    The free times of a day as "HH:mm" wall times in context.timeZone (or { time, disabled, label }). Gets the service, the team member (null for anyone) and the zone. A promise shows skeleton slots. Pass a stable function. Without it every slot from startTime to endTime is free.

  • startTimedefault "09:00"
    string

    The first slot of a day when there is no getTimeSlots.

  • endTimedefault "17:00"
    string

    When the day ends without getTimeSlots; the last slot still finishes by then.

  • minuteStepdefault 30
    number

    Minutes between slots without getTimeSlots.

  • disabledDaysdefault "DEFAULT_BOOKING_DISABLED_DAYS"
    DateMatcher or DateMatcher[] (DateMatcher: Date, Date[], { from, to }, { before }, { after }, { dayOfWeek } or (date) => boolean)

    Days that cannot be booked, as calendar matchers. Weekends by default; past days are always off.

  • bookingWindowdefault 60
    number

    How many days ahead can be booked. 0 means no limit.

  • defaultValuedefault "null"
    Date | null

    The start picked at first, e.g. the booking being moved. The details step opens with it filled in.

  • defaultDetails
    Partial<BookingDetails>

    Prefilled name, email, phone (E.164) and notes, for a signed-in visitor.

  • defaultDaydefault ""
    string

    The day the calendar opens on, as "YYYY-MM-DD", with its times listed before one is picked. Empty: no day until the visitor picks one.

  • timeZone
    string

    The IANA zone the times are shown in, controlled. Pair it with onTimeZoneChange.

  • defaultTimeZonedefault ""
    string

    The starting zone while uncontrolled. Empty: the visitor's device zone.

  • onTimeZoneChange
    (zone: string) => void

    Fires when the visitor switches the zone in the picker footer.

  • timeZonesdefault "["America/New_York", "America/Chicago", "America/Denver", "America/Los_Angeles", "Europe/London", "Europe/Berlin", "Asia/Tokyo"]"
    string[]

    The zones the switch offers (the current one is always included). [] shows the zone as plain text.

  • hourCycledefault "auto"
    "auto" | "h12" | "h23"

    12 or 24 hour times; auto follows the locale.

  • step
    "service" | "time" | "details" | "confirmation"

    The current step, controlled. A step that is not reachable yet (details without a time) falls back to the first missing one.

  • defaultStepdefault "service"
    "service" | "time" | "details" | "confirmation"

    The starting step while uncontrolled.

  • onStepChange
    (step: BookingStep) => void

    Fires on Continue, Back, a step click and after booking.

  • layoutdefault "split"
    "split" | "stacked"

    split: the summary column beside the steps from 768px. stacked: the summary as a header row at every width, in a narrower card. Phones always stack.

  • showPhonedefault true
    boolean

    Shows the phone field (with a country picker) in the details step.

  • requirePhonedefault false
    boolean

    Makes the phone required. Optional phones are still checked when filled in.

  • showNotesdefault true
    boolean

    Shows the optional notes field.

  • defaultCountrydefault "US"
    string

    The phone field's starting country, as an ISO code ("US", "GB").

  • locationdefault "Video call"
    string

    Where the meeting happens, for every service that has no location of its own.

  • locationTypedefault "video"
    "video" | "phone" | "in-person"

    The icon next to the location.

  • currencydefault "USD"
    string

    The ISO currency of the prices. A price of 0 shows the free label.

  • localedefault ""
    string

    Formats dates, times and prices. Empty: the UIProvider locale, then the browser's.

  • onBook
    (booking: Booking) => void | Promise<unknown>

    Called by Confirm booking with { service, staff, start, end, timeZone, details, bookedAt, previous? }. A promise keeps the button loading; a rejection shows the bookFailed message. Default: a pretend 900ms request.

  • rescheduleHref
    string | ((booking: Booking) => string)

    Turns Reschedule into a link (your reschedule page). Without it Reschedule goes back to the times here.

  • icsFileNamedefault "booking.ics"
    string

    The file name of the Add to calendar download.

  • tonedefault "neutral"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    Paints the main button, the selected cards, the steps, the picked day and time.

  • colordefault ""
    string

    Any CSS color; replaces the tone everywhere. Leave empty to use the tone.

  • radiusdefault "lg"
    "none" | "sm" | "md" | "lg"

    The card's corners: 0, 12, 16 or 24px.

  • now
    Date | number

    What now means for past days and the booking window. Default: the clock.

  • labels
    Partial<BookingWidgetLabels>

    Every string (DEFAULT_BOOKING_WIDGET_LABELS holds the English defaults): step names and headings, timeDescription(days), field labels and placeholders, buttons, the confirmation, the duration formatter and the error messages.

  • classNames
    Partial<Record<"summary" | "header" | "host" | "main" | "steps" | "body" | "title" | "services" | "staff" | "picker" | "fields" | "error" | "actions" | "confirmation" | "recap", string>>

    Extra classes for the inner parts.

  • className
    string

    Extra classes for the card, merged last (max-w-[960px] for split, max-w-xl for stacked).

Parts

  • buildBookingIcs

    function

    Turns a Booking into an iCalendar file (the Add to calendar download). Use it to attach the same file to your confirmation email.

    • booking*
      Booking

      What onBook received.

    • options
      { title?: string; location?: string; description?: string; organizer?: string }

      The event title (default "<service> with <person>"), location, description and the organizer name.

  • DEFAULT_BOOKING_SERVICES

    constant

    The three sample services: Intro call (free), Consultation and Strategy session.

  • DEFAULT_BOOKING_DISABLED_DAYS

    constant

    The default disabledDays: { dayOfWeek: [0, 6] }, weekends off.

  • DEFAULT_BOOKING_HOST

    constant

    The sample host, Emma Carter.

  • DEFAULT_BOOKING_TIME_ZONES

    constant

    The zones the switch offers by default: four US zones, London, Berlin and Tokyo.

  • DEFAULT_BOOKING_WIDGET_LABELS

    constant

    The English labels; spread it to change a few.

  • Booking

    type

    What onBook gets: service, staff, start, end, timeZone, details, bookedAt and previous after a reschedule.

  • BookingService

    type

    One bookable service.

  • BookingStaff

    type

    One team member and the services they offer.

  • BookingHost

    type

    The person in the summary.

  • BookingDetails

    type

    The visitor's name, email, phone (E.164) and notes.

  • BookingSlotContext

    type

    The second argument of getTimeSlots: service, staff and timeZone.

  • BookingTimeSlots

    type

    The getTimeSlots signature.

  • BookingStep

    type

    "service" | "time" | "details" | "confirmation".

  • BookingWidgetLabels

    type

    The shape of labels.

Examples

Open

Salon with stylists

Services with icons, stylists filtered by what they offer, an in-person address and a required phone.

Open

One service, stacked

With a single service and no staff the flow starts at the times; the summary is a header row.

Open

Loading availability

getTimeSlots returns a promise, so each day shows skeleton slots first. Brand tone.

Open

When booking fails

The first onBook rejects: the error shows above the actions and the details stay. Try again to book.

Open

Controlled step, custom color

step and onStepChange in your state, a violet color, 24 hour times in London and prices in pounds.

Open

Romanian locale

ro-RO dates and RON prices, a Romanian phone by default and a short zone list.

Dependencies

motionlucide-reactavatarbuttondate-time-pickerinputphone-inputradio-groupstepstextareause-controllable-stateuse-localemotionrelative-timetonesui-config

Files

  • components/blocks/booking-widget.tsx

FAQ

Is the React Booking Widget block free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Booking Widget?

Run npx wingo-ui@latest add booking-widget. It also adds the Wingo UI items it builds on (Avatar, Button, Date Time Picker, Input, Phone Input and 9 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/booking-widget.

How does Booking Widget behave on phones?

Below 768px the card stacks: the summary becomes a header row with the avatar, service, length and the picked time, the times drop under the calendar, and the Back / Continue bar sticks to the bottom of the screen above the safe area.

See also

Date Time PickerTimezone SelectStepsDemo Booking PageCheckout steps

Used in

Appointment Booking Page

More React form components

CSV ImportFieldForm WizardInline EditInvoice line itemsRich Text Editor
All 9 React form components
PreviousAddress InputNext CSV Import