Back to Widget Patterns
v2.6Interactive Tooling

Scheduling Tool

Calendly-style date and time picker with a contact form. Auto-generates default availability or accepts pre-defined slots. Three-step flow: date, time, contact details, confirmation.

Live preview

Rendered with real component code. Each example demonstrates a documented variant.

30-minute intro call

Book a 30-minute intro call

30 minute meeting

1. Pick a date

Custom slots plus a notes field

Schedule a strategy session

45 minute meeting

Pick a time and tell us what you would like to cover.

1. Pick a date

React props

Configure the component via props in React or via attributes in HTML.

PropTypeDefaultDescription
onConfirm(selection: BookingSelection) => Promise<void>requiredCalled on submit with the selected date, time, duration, and form values; wire your booking backend here
titlestringundefinedHeading shown at the top of the scheduling widget
descriptionstringundefinedSupporting copy shown below the title
durationnumber30Meeting length in minutes; shown as display text and passed to onConfirm
availableSlotsAvailableSlot[]auto-generatedExplicit list of dates and times; omit to auto-generate the next 14 weekdays with standard morning and afternoon slots
formFieldsFormField[]name + emailContact fields shown after a time is picked; supports text, email, tel, and textarea types
timezonestring"Your local timezone"Display label shown below the form to set timezone expectations
successTextstring"Booking confirmed"Message shown after a successful onConfirm call
classNamestringundefinedExtra class applied to the root section element

HTML usage

The HTML variant uses the same shared styles and class names. Drop into any stack.

<link rel="stylesheet" href="path/to/primary-button-cta/styles.css">
<link rel="stylesheet" href="path/to/scheduling-tool/styles.css">

<section class="sch" id="booking-widget">
  <h2 class="sch__title">Book a 30-minute intro call</h2>
  <p class="sch__duration">30 minute meeting</p>

  <p class="sch__step-label">1. Pick a date</p>
  <div class="sch__dates" role="group" aria-label="Available dates">
    <button class="sch__date" type="button" aria-pressed="false">
      <span class="sch__date-weekday">Mon</span>
      <span class="sch__date-day">19</span>
      <span class="sch__date-month">May</span>
    </button>
    <!-- Repeat for each available date -->
  </div>

  <!-- Revealed after a date is selected -->
  <p class="sch__step-label">2. Pick a time</p>
  <div class="sch__times" role="group" aria-label="Available times">
    <button class="sch__time" type="button" aria-pressed="false">9:00 AM</button>
    <button class="sch__time" type="button" aria-pressed="false">9:30 AM</button>
  </div>

  <!-- Revealed after a time is selected -->
  <form class="sch__form" id="booking-form">
    <p class="sch__step-label">3. Your details</p>
    <div class="sch__field">
      <label class="sch__field-label" for="sch-name">Name <span aria-hidden="true"> *</span></label>
      <input class="sch__field-input" id="sch-name" type="text" name="name" required>
    </div>
    <div class="sch__field">
      <label class="sch__field-label" for="sch-email">Email <span aria-hidden="true"> *</span></label>
      <input class="sch__field-input" id="sch-email" type="email" name="email" required>
    </div>
    <div class="sch__actions">
      <button class="pbc pbc--solid pbc--rounded" type="submit">Confirm booking</button>
    </div>
    <p class="sch__timezone">Your local timezone</p>
  </form>
</section>

<script>
(function () {
  var dates = document.querySelectorAll('.sch__date');
  var timesSection = document.querySelector('.sch__times');
  var form = document.getElementById('booking-form');
  var selectedDate = null;
  var selectedTime = null;

  dates.forEach(function (btn) {
    btn.addEventListener('click', function () {
      dates.forEach(function (b) { b.setAttribute('aria-pressed', 'false'); });
      btn.setAttribute('aria-pressed', 'true');
      selectedDate = btn.dataset.date;
      timesSection.hidden = false;
    });
  });

  document.querySelectorAll('.sch__time').forEach(function (btn) {
    btn.addEventListener('click', function () {
      document.querySelectorAll('.sch__time').forEach(function (b) { b.setAttribute('aria-pressed', 'false'); });
      btn.setAttribute('aria-pressed', 'true');
      selectedTime = btn.dataset.time;
      form.hidden = false;
    });
  });

  form.addEventListener('submit', function (e) {
    e.preventDefault();
    var submitBtn = form.querySelector('[type="submit"]');
    submitBtn.disabled = true;
    submitBtn.textContent = 'Booking...';
    fetch('/api/book', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ date: selectedDate, time: selectedTime }),
    }).then(function () {
      document.getElementById('booking-widget').innerHTML =
        '<p class="sch__success-message">Booking confirmed</p>';
    });
  });
}());
</script>

Customization

Override these CSS custom properties to integrate the component into your brand.

.sch {
  --sch-bg: var(--brand-surface, white);
  --sch-text-color: var(--brand-ink, #102542);
  --sch-muted-color: rgba(16, 37, 66, 0.6);
  --sch-accent: var(--brand-accent, #1e5fcf);
  --sch-date-bg: white;
  --sch-date-border: rgba(0, 0, 0, 0.1);
  --sch-date-bg-selected: var(--sch-accent);
  --sch-date-text-selected: white;
  --sch-date-bg-hover: rgba(0, 0, 0, 0.025);
  --sch-time-bg: white;
  --sch-time-border: rgba(0, 0, 0, 0.1);
  --sch-time-bg-selected: var(--sch-accent);
  --sch-time-text-selected: white;
  --sch-input-border: rgba(0, 0, 0, 0.12);
  --sch-radius: 0.75rem;
  --sch-section-padding: 2rem;
  --sch-gap: 1rem;
}