Skip to content
Product block

Meeting Scheduler

Day strip, slot grid and a time zone the visitor can change — because a slot list with no zone on it is how this pattern wastes an hour of everyone involved.

334 lineslucide-reactAdded 25 Aug 2026Updated 13 Sept 2026
  • booking
  • calendar
  • scheduler
  • time zone
  • meeting

What's included

  • components/booking-scheduler.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add booking-scheduler

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Book a walkthrough

Thirty minutes with an engineer, not a sales team. Bring your codebase.

  • 30 minutes
  • Video call, link on confirmation
Pick a day
Pick a time

Pick a time to continue.

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/booking-scheduler.tsx
'use client'

/**
 * <BookingScheduler> — pick a day, pick a time, and know which time it is.
 *
 * The two Contact & Forms blocks here send a message and wait. Plenty of
 * teams do not want a message — they want the call booked — and the moment
 * a "contact us" form becomes a calendar, three problems arrive that a
 * message form never had.
 *
 * TIME ZONES, WHICH IS THE WHOLE PROBLEM
 *
 * A slot list with no zone on it is the single most common way this pattern
 * wastes somebody's morning. Every time here is rendered in the visitor's
 * own zone, the zone is named above the slots, and it is a control rather
 * than a label — because the person booking is often not in the zone their
 * laptop is set to, and the ones who are travelling are exactly the ones a
 * missed call costs the most.
 *
 * The zone is resolved on the client, after mount, deliberately. Reading
 * `Intl.DateTimeFormat().resolvedOptions().timeZone` during a server render
 * gives the server's zone — UTC on most hosts — and a first paint that says
 * "times shown in UTC" to someone in Mumbai is worse than a first paint
 * that says nothing. Until it resolves, the label is neutral.
 *
 * A STRIP OF DAYS, NOT A MONTH VIEW
 *
 * A full month calendar is mostly cells you cannot click. A horizontally
 * scrollable strip of the days that actually have availability answers the
 * real question — "when could I see them" — at a glance, and it works at
 * 320px, where a seven-column month view starts hiding its own controls.
 *
 * Fully booked days stay in the strip rather than being filtered out. A
 * gap where Wednesday should be reads as a rendering fault; a Wednesday
 * marked "Full" reads as information, and it is the thing that makes the
 * free days next to it look worth taking.
 *
 * ACCESSIBILITY
 *
 * The slots are a radio group, not a grid of buttons. That is what gives
 * arrow-key movement between times, one tab stop for the whole set, and an
 * announced group name — all of it from the platform, none of it
 * hand-rolled. Days are the same. Full dates are in each label for screen
 * readers even though the visible text is abbreviated, because "14" is not
 * a date.
 */

import * as React from 'react'
import { Calendar, Check, Clock, Globe, Video } from 'lucide-react'

export interface BookingDay {
  /** ISO date, YYYY-MM-DD. */
  date: string
  /** Times as 24h "HH:MM" in the host's zone. Empty means fully booked. */
  slots: string[]
}

export interface BookingSchedulerProps {
  heading?: string
  description?: string
  /** Length of the meeting, in minutes — shown, never inferred. */
  durationMinutes?: number
  /** IANA zone the `slots` are expressed in. */
  hostTimeZone?: string
  days?: BookingDay[]
  onConfirm?: (choice: { date: string; time: string; timeZone: string }) => void
  className?: string
}

/*
  A fortnight of availability with realistic gaps: a fully booked Tuesday,
  thin Fridays, nothing at weekends. A demo where every slot is free makes
  the empty state look like a bug the first time a real calendar renders.
*/
const DEFAULT_DAYS: BookingDay[] = [
  { date: '2026-09-01', slots: ['09:00', '09:30', '11:00', '14:00', '15:30'] },
  { date: '2026-09-02', slots: [] },
  { date: '2026-09-03', slots: ['10:00', '10:30', '13:00', '16:00'] },
  { date: '2026-09-04', slots: ['09:30', '12:00'] },
  { date: '2026-09-05', slots: ['15:00'] },
  { date: '2026-09-08', slots: ['09:00', '11:30', '13:30', '14:00', '16:30'] },
  { date: '2026-09-09', slots: ['10:00', '10:30', '11:00', '15:00'] },
  { date: '2026-09-10', slots: ['09:00', '13:00'] },
]

const ZONES = [
  'America/Los_Angeles',
  'America/New_York',
  'Europe/London',
  'Europe/Berlin',
  'Asia/Kolkata',
  'Asia/Singapore',
  'Australia/Sydney',
]

export function BookingScheduler({
  heading = 'Book a walkthrough',
  description = 'Thirty minutes with an engineer, not a sales team. Bring your codebase.',
  durationMinutes = 30,
  hostTimeZone = 'Europe/London',
  days = DEFAULT_DAYS,
  onConfirm,
  className = '',
}: BookingSchedulerProps) {
  /*
    Ids come from `useId`, not from a literal.

    A literal is correct until the section appears twice on one page — and
    then `aria-labelledby` resolves to whichever copy is first in the
    document, so the second one is announced with the first one's heading.
    It renders identically and reads wrongly, which is why nothing catches
    it but an id check.
  */
  const headingId = React.useId()

  const firstOpen = days.find((day) => day.slots.length > 0)
  const [selectedDate, setSelectedDate] = React.useState(firstOpen?.date ?? days[0]?.date ?? '')
  const [selectedTime, setSelectedTime] = React.useState('')

  /*
    Resolved after mount, never during render. See the note above: the
    server's zone is not the visitor's, and a wrong zone stated confidently
    is worse than no zone stated yet.
  */
  const [zone, setZone] = React.useState<string | null>(null)
  React.useEffect(() => {
    setZone(Intl.DateTimeFormat().resolvedOptions().timeZone)
  }, [])

  const activeZone = zone ?? hostTimeZone
  const day = days.find((d) => d.date === selectedDate)

  /** A host-zone "HH:MM" on `date`, as an absolute instant. */
  function instant(date: string, time: string): Date {
    /*
      Built from the host zone by measuring its offset on that date rather
      than hard-coding one. Offsets move twice a year, and a scheduler that
      is right in January and an hour out in July is the classic version of
      this bug.
    */
    const naive = new Date(`${date}T${time}:00Z`)
    const asHost = new Date(
      naive.toLocaleString('en-US', { timeZone: hostTimeZone }),
    )
    const asUtc = new Date(naive.toLocaleString('en-US', { timeZone: 'UTC' }))
    return new Date(naive.getTime() + (asUtc.getTime() - asHost.getTime()))
  }

  function formatTime(date: string, time: string): string {
    return instant(date, time).toLocaleTimeString('en-US', {
      hour: 'numeric',
      minute: '2-digit',
      timeZone: activeZone,
    })
  }

  const dayLabel = (iso: string, opts: Intl.DateTimeFormatOptions) =>
    new Date(`${iso}T12:00:00Z`).toLocaleDateString('en-US', {
      ...opts,
      timeZone: 'UTC',
    })

  return (
    <section
      aria-labelledby={headingId}
      className={`mx-auto w-full max-w-3xl px-4 py-16 sm:px-6 lg:px-8 ${className}`}
    >
      <div className="overflow-hidden rounded-2xl border border-border bg-card">
        <header className="border-b border-border px-6 py-5">
          <h2 id={headingId} className="text-lg font-semibold text-foreground">
            {heading}
          </h2>
          <p className="mt-1 text-sm text-muted-foreground">{description}</p>

          <ul className="mt-4 flex flex-wrap gap-x-5 gap-y-2 text-sm text-muted-foreground">
            <li className="flex items-center gap-1.5">
              <Clock aria-hidden className="h-4 w-4" />
              {durationMinutes} minutes
            </li>
            <li className="flex items-center gap-1.5">
              <Video aria-hidden className="h-4 w-4" />
              Video call, link on confirmation
            </li>
          </ul>
        </header>

        <div className="px-6 py-5">
          <fieldset>
            <legend className="flex items-center gap-2 text-sm font-medium text-foreground">
              <Calendar aria-hidden className="h-4 w-4 text-muted-foreground" />
              Pick a day
            </legend>

            <div className="relative mt-3 flex gap-2 overflow-x-auto pb-2">
              {days.map((option) => {
                const open = option.slots.length > 0
                const active = option.date === selectedDate
                return (
                  <label
                    key={option.date}
                    className={`flex min-w-[4.5rem] shrink-0 cursor-pointer flex-col items-center rounded-xl border px-3 py-2.5 text-center transition ${
                      active
                        ? 'border-primary bg-primary/10 text-foreground'
                        : open
                          ? 'border-border bg-background text-foreground hover:bg-muted'
                          : 'cursor-not-allowed border-border/60 bg-muted/40 text-muted-foreground'
                    } focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background`}
                  >
                    <input
                      type="radio"
                      name="booking-day"
                      value={option.date}
                      checked={active}
                      disabled={!open}
                      onChange={() => {
                        setSelectedDate(option.date)
                        setSelectedTime('')
                      }}
                      className="sr-only"
                    />
                    <span className="text-xs uppercase tracking-wide">
                      {dayLabel(option.date, { weekday: 'short' })}
                    </span>
                    <span className="text-lg font-semibold leading-tight">
                      {dayLabel(option.date, { day: 'numeric' })}
                    </span>
                    <span className="text-[11px] text-muted-foreground">
                      {open ? `${option.slots.length} free` : 'Full'}
                    </span>
                    <span className="sr-only">
                      {dayLabel(option.date, {
                        weekday: 'long',
                        day: 'numeric',
                        month: 'long',
                      })}
                      {open ? '' : ', fully booked'}
                    </span>
                  </label>
                )
              })}
            </div>
          </fieldset>

          <fieldset className="mt-6">
            <div className="flex flex-wrap items-center justify-between gap-3">
              <legend className="float-start text-sm font-medium text-foreground">
                Pick a time
              </legend>

              {/* A control, not a label. Someone booking from an airport is
                  the person this matters most to. */}
              <label className="ms-auto flex items-center gap-2 text-xs text-muted-foreground">
                <Globe aria-hidden className="h-3.5 w-3.5" />
                <span className="sr-only">Show times in this time zone</span>
                <select
                  value={activeZone}
                  onChange={(event) => setZone(event.target.value)}
                  className="h-8 rounded-lg border border-field bg-background px-2 text-xs text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
                >
                  {[...new Set([activeZone, ...ZONES])].map((tz) => (
                    <option key={tz} value={tz}>
                      {tz.replace(/_/g, ' ')}
                    </option>
                  ))}
                </select>
              </label>
            </div>

            {day && day.slots.length > 0 ? (
              <div className="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-3">
                {day.slots.map((slot) => {
                  const active = slot === selectedTime
                  return (
                    <label
                      key={slot}
                      className={`flex cursor-pointer items-center justify-center rounded-lg border px-3 py-2 text-sm font-medium transition ${
                        active
                          ? 'border-primary bg-primary text-primary-foreground'
                          : 'border-border bg-background text-foreground hover:bg-muted'
                      } focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background`}
                    >
                      <input
                        type="radio"
                        name="booking-slot"
                        value={slot}
                        checked={active}
                        onChange={() => setSelectedTime(slot)}
                        className="sr-only"
                      />
                      {formatTime(day.date, slot)}
                    </label>
                  )
                })}
              </div>
            ) : (
              <p className="mt-3 rounded-lg border border-dashed border-border px-4 py-6 text-center text-sm text-muted-foreground">
                Nothing free that day. Try another.
              </p>
            )}
          </fieldset>
        </div>

        <footer className="flex flex-wrap items-center justify-between gap-3 border-t border-border bg-muted/30 px-6 py-4">
          <p role="status" className="text-sm text-muted-foreground">
            {selectedTime && day ? (
              <>
                <Check aria-hidden className="me-1.5 inline h-4 w-4 text-foreground" />
                {dayLabel(day.date, { weekday: 'long', day: 'numeric', month: 'long' })} at{' '}
                <span className="font-medium text-foreground">
                  {formatTime(day.date, selectedTime)}
                </span>{' '}
                ({activeZone.replace(/_/g, ' ')})
              </>
            ) : (
              'Pick a time to continue.'
            )}
          </p>

          <button
            type="button"
            disabled={!selectedTime}
            onClick={() =>
              onConfirm?.({ date: selectedDate, time: selectedTime, timeZone: activeZone })
            }
            className="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-semibold text-primary-foreground transition hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50"
          >
            Confirm booking
          </button>
        </footer>
      </div>
    </section>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/booking-scheduler.tsx and import it where you need the section:

import { BookingScheduler } from '@/components/booking-scheduler'

Customize

4 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
headingstring'Book a walkthrough'
descriptionstring'Thirty minutes with an engineer, not a sales…
durationMinutesLength of the meeting, in minutes — shown, never inferred.number30
hostTimeZoneIANA zone the `slots` are expressed in.string'Europe/London'
daysBookingDay[]DEFAULT_DAYS
onConfirm(choice: { date: string; time: string; timeZone: string }) => void—
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.

booking-scheduler.html
<!--
  Meeting Scheduler — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block is interactive in React and the handlers are NOT here.
  Buttons, toggles and menus render in their initial state and do
  nothing until you wire them up.
-->
<section aria-labelledby="_R_0_" class="mx-auto w-full max-w-3xl px-4 py-16 sm:px-6 lg:px-8 ">
  <div class="overflow-hidden rounded-2xl border border-border bg-card">
    <header class="border-b border-border px-6 py-5">
      <h2 id="_R_0_" class="text-lg font-semibold text-foreground">Book a walkthrough</h2>
      <p class="mt-1 text-sm text-muted-foreground">Thirty minutes with an engineer, not a sales team. Bring your codebase.</p>
      <ul class="mt-4 flex flex-wrap gap-x-5 gap-y-2 text-sm text-muted-foreground">
        <li class="flex items-center gap-1.5"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock h-4 w-4" aria-hidden="true"><path d="M12 6v6l4 2"></path><circle cx="12" cy="12" r="10"></circle></svg>30 minutes</li>
        <li class="flex items-center gap-1.5"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-video h-4 w-4" aria-hidden="true"><path d="m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5"></path><rect x="2" y="6" width="14" height="12" rx="2"></rect></svg>Video call, link on confirmation</li>
      </ul>
    </header>
    <div class="px-6 py-5">
      <fieldset>
        <legend class="flex items-center gap-2 text-sm font-medium text-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar h-4 w-4 text-muted-foreground" aria-hidden="true"><path d="M8 2v4"></path><path d="M16 2v4"></path><rect width="18" height="18" x="3" y="4" rx="2"></rect><path d="M3 10h18"></path></svg>Pick a day</legend>
        <div class="relative mt-3 flex gap-2 overflow-x-auto pb-2">
          <label class="flex min-w-[4.5rem] shrink-0 cursor-pointer flex-col items-center rounded-xl border px-3 py-2.5 text-center transition border-primary bg-primary/10 text-foreground focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background">
            <input type="radio" class="sr-only" name="booking-day" checked="" value="2026-09-01" />
            <span class="text-xs uppercase tracking-wide">Tue</span>
            <span class="text-lg font-semibold leading-tight">1</span>
            <span class="text-[11px] text-muted-foreground">5 free</span>
            <span class="sr-only">Tuesday, September 1</span>
          </label>
          <label class="flex min-w-[4.5rem] shrink-0 cursor-pointer flex-col items-center rounded-xl border px-3 py-2.5 text-center transition cursor-not-allowed border-border/60 bg-muted/40 text-muted-foreground focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background">
            <input type="radio" disabled="" class="sr-only" name="booking-day" value="2026-09-02" />
            <span class="text-xs uppercase tracking-wide">Wed</span>
            <span class="text-lg font-semibold leading-tight">2</span>
            <span class="text-[11px] text-muted-foreground">Full</span>
            <span class="sr-only">Wednesday, September 2, fully booked</span>
          </label>
          <label class="flex min-w-[4.5rem] shrink-0 cursor-pointer flex-col items-center rounded-xl border px-3 py-2.5 text-center transition border-border bg-background text-foreground hover:bg-muted focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background">
            <input type="radio" class="sr-only" name="booking-day" value="2026-09-03" />
            <span class="text-xs uppercase tracking-wide">Thu</span>
            <span class="text-lg font-semibold leading-tight">3</span>
            <span class="text-[11px] text-muted-foreground">4 free</span>
            <span class="sr-only">Thursday, September 3</span>
          </label>
          <label class="flex min-w-[4.5rem] shrink-0 cursor-pointer flex-col items-center rounded-xl border px-3 py-2.5 text-center transition border-border bg-background text-foreground hover:bg-muted focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background">
            <input type="radio" class="sr-only" name="booking-day" value="2026-09-04" />
            <span class="text-xs uppercase tracking-wide">Fri</span>
            <span class="text-lg font-semibold leading-tight">4</span>
            <span class="text-[11px] text-muted-foreground">2 free</span>
            <span class="sr-only">Friday, September 4</span>
          </label>
          <label class="flex min-w-[4.5rem] shrink-0 cursor-pointer flex-col items-center rounded-xl border px-3 py-2.5 text-center transition border-border bg-background text-foreground hover:bg-muted focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background">
            <input type="radio" class="sr-only" name="booking-day" value="2026-09-05" />
            <span class="text-xs uppercase tracking-wide">Sat</span>
            <span class="text-lg font-semibold leading-tight">5</span>
            <span class="text-[11px] text-muted-foreground">1 free</span>
            <span class="sr-only">Saturday, September 5</span>
          </label>
          <label class="flex min-w-[4.5rem] shrink-0 cursor-pointer flex-col items-center rounded-xl border px-3 py-2.5 text-center transition border-border bg-background text-foreground hover:bg-muted focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background">
            <input type="radio" class="sr-only" name="booking-day" value="2026-09-08" />
            <span class="text-xs uppercase tracking-wide">Tue</span>
            <span class="text-lg font-semibold leading-tight">8</span>
            <span class="text-[11px] text-muted-foreground">5 free</span>
            <span class="sr-only">Tuesday, September 8</span>
          </label>
          <label class="flex min-w-[4.5rem] shrink-0 cursor-pointer flex-col items-center rounded-xl border px-3 py-2.5 text-center transition border-border bg-background text-foreground hover:bg-muted focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background">
            <input type="radio" class="sr-only" name="booking-day" value="2026-09-09" />
            <span class="text-xs uppercase tracking-wide">Wed</span>
            <span class="text-lg font-semibold leading-tight">9</span>
            <span class="text-[11px] text-muted-foreground">4 free</span>
            <span class="sr-only">Wednesday, September 9</span>
          </label>
          <label class="flex min-w-[4.5rem] shrink-0 cursor-pointer flex-col items-center rounded-xl border px-3 py-2.5 text-center transition border-border bg-background text-foreground hover:bg-muted focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background">
            <input type="radio" class="sr-only" name="booking-day" value="2026-09-10" />
            <span class="text-xs uppercase tracking-wide">Thu</span>
            <span class="text-lg font-semibold leading-tight">10</span>
            <span class="text-[11px] text-muted-foreground">2 free</span>
            <span class="sr-only">Thursday, September 10</span>
          </label>
        </div>
      </fieldset>
      <fieldset class="mt-6">
        <div class="flex flex-wrap items-center justify-between gap-3">
          <legend class="float-start text-sm font-medium text-foreground">Pick a time</legend>
          <label class="ms-auto flex items-center gap-2 text-xs text-muted-foreground">
            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe h-3.5 w-3.5" aria-hidden="true">
              <circle cx="12" cy="12" r="10"></circle>
              <path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"></path>
              <path d="M2 12h20"></path>
            </svg>
            <span class="sr-only">Show times in this time zone</span>
            <select class="h-8 rounded-lg border border-field bg-background px-2 text-xs text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background">
              <option value="Europe/London" selected="">Europe/London</option>
              <option value="America/Los_Angeles">America/Los Angeles</option>
              <option value="America/New_York">America/New York</option>
              <option value="Europe/Berlin">Europe/Berlin</option>
              <option value="Asia/Kolkata">Asia/Kolkata</option>
              <option value="Asia/Singapore">Asia/Singapore</option>
              <option value="Australia/Sydney">Australia/Sydney</option>
            </select>
          </label>
        </div>
        <div class="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-3">
          <label class="flex cursor-pointer items-center justify-center rounded-lg border px-3 py-2 text-sm font-medium transition border-border bg-background text-foreground hover:bg-muted focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background"><input type="radio" class="sr-only" name="booking-slot" value="09:00" />9:00 AM</label>
          <label class="flex cursor-pointer items-center justify-center rounded-lg border px-3 py-2 text-sm font-medium transition border-border bg-background text-foreground hover:bg-muted focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background"><input type="radio" class="sr-only" name="booking-slot" value="09:30" />9:30 AM</label>
          <label class="flex cursor-pointer items-center justify-center rounded-lg border px-3 py-2 text-sm font-medium transition border-border bg-background text-foreground hover:bg-muted focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background"><input type="radio" class="sr-only" name="booking-slot" value="11:00" />11:00 AM</label>
          <label class="flex cursor-pointer items-center justify-center rounded-lg border px-3 py-2 text-sm font-medium transition border-border bg-background text-foreground hover:bg-muted focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background"><input type="radio" class="sr-only" name="booking-slot" value="14:00" />2:00 PM</label>
          <label class="flex cursor-pointer items-center justify-center rounded-lg border px-3 py-2 text-sm font-medium transition border-border bg-background text-foreground hover:bg-muted focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background"><input type="radio" class="sr-only" name="booking-slot" value="15:30" />3:30 PM</label>
        </div>
      </fieldset>
    </div>
    <footer class="flex flex-wrap items-center justify-between gap-3 border-t border-border bg-muted/30 px-6 py-4">
      <p role="status" class="text-sm text-muted-foreground">Pick a time to continue.</p>
      <button type="button" disabled="" class="inline-flex h-9 items-center rounded-lg bg-primary px-4 text-sm font-semibold text-primary-foreground transition hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50">Confirm booking</button>
    </footer>
  </div>
</section>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • This block is interactive in React — toggles, menus or form state. None of that survives here; the markup is the closed/default state and the handlers are gone. Re-wire them in your own framework.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

Used in these pages

Want the whole screen instead of this one section? Open a page and copy it entire.

2 more blocks in Scheduling & Calendar

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse Scheduling & Calendar

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More Scheduling & Calendar blocks

View category
Open the full page for this block

Month Calendar Grid

A month view where event chips truncate and overflow becomes "+n more", so a busy Tuesday never changes the height of the row it sits in.

Scheduling & Calendar191 lines1 dep
Open the full page for this block

Conference Agenda Grid

Time down the side, tracks across, breaks spanning the width — the one comparison a month view cannot make. Falls to a linear list on a phone, from the same array.

Scheduling & Calendar397 lines1 dep