Forms & Lead Capture

Free Booking Request Form Widget

Let visitors request an appointment with a date and time window.

A compact booking request form for service businesses — visitors pick a preferred date and time window, leave their name and phone or email, and the request lands in your inbox or form backend. It POSTs to any endpoint you choose (Formspree, Basin, your own API). Install is one script tag wherever you want the form to appear.

Live demo

Embed this widget

Paste either snippet into your page — a Custom HTML block on WordPress, Shopify, or Squarespace works too. Both snippets update live as you customize, so what you copy is exactly what you configured.

Hosted (recommended)html
<script src="https://hayneslab.dev/widgets/booking-request-form.js" data-hl-action="https://formspree.io/f/your-form-id" data-hl-success-message="Thanks! We'll call or email you within one business day to confirm." async></script>
<!-- Free Booking Request Form widget by haynes lab — https://hayneslab.dev/tools/widgets/booking-request-form -->
Inline (self-contained)html
<!-- Free Booking Request Form widget by haynes lab — https://hayneslab.dev/tools/widgets/booking-request-form -->
<script data-hl-action="https://formspree.io/f/your-form-id" data-hl-success-message="Thanks! We'll call or email you within one business day to confirm.">
/*!
 * haynes lab — Booking Request Form widget (free)
 * Docs & embed code: https://hayneslab.dev/tools/widgets/booking-request-form
 */
(function () {
  'use strict';

  var PREFIX = 'hlw-booking-request-form';
  var STYLE_ID = 'hlw-booking-request-form-styles';

  function findScript() {
    return document.currentScript ||
      document.querySelector('script[src*="widgets/booking-request-form.js"]');
  }

  function injectStyles() {
    if (document.getElementById(STYLE_ID)) return;
    var css =
      '.' + PREFIX + ', .' + PREFIX + ' * { box-sizing: border-box; }' +
      '.' + PREFIX + ' { position: relative; max-width: 460px; width: 100%; margin: 0 auto; padding: 28px 28px 30px;' +
      ' font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;' +
      ' font-size: 16px; line-height: 1.5; color: #1f2937; background: #ffffff;' +
      ' border: 1px solid #e5e7eb; border-radius: 12px; }' +
      '.' + PREFIX + '__title { display: block; font-size: 20px; font-weight: 700; color: #111827; margin: 0 0 4px; }' +
      '.' + PREFIX + '__subtitle { font-size: 14px; color: #6b7280; margin: 0 0 18px; }' +
      '.' + PREFIX + '__field { display: block; margin-bottom: 14px; }' +
      '.' + PREFIX + '__label { display: block; font-size: 13px; font-weight: 600; color: #374151; margin-bottom: 5px; }' +
      '.' + PREFIX + '__input { display: block; width: 100%; font: inherit; font-size: 15px; padding: 10px 12px;' +
      ' border: 1px solid #d1d5db; border-radius: 8px; background: #ffffff; color: #111827; }' +
      '.' + PREFIX + '__input:focus { outline: 2px solid var(--hlw-accent); outline-offset: 1px; border-color: var(--hlw-accent); }' +
      '.' + PREFIX + '__row { display: flex; gap: 12px; }' +
      '.' + PREFIX + '__row .' + PREFIX + '__field { flex: 1 1 0; min-width: 0; }' +
      '.' + PREFIX + '__btn { display: block; width: 100%; font: inherit; font-size: 15px; font-weight: 600;' +
      ' padding: 11px 18px; margin-top: 4px; border: 0; border-radius: 8px; background: var(--hlw-accent);' +
      ' color: #ffffff; cursor: pointer; }' +
      '.' + PREFIX + '__btn:hover { filter: brightness(1.08); }' +
      '.' + PREFIX + '__btn:focus-visible { outline: 2px solid var(--hlw-accent); outline-offset: 2px; }' +
      '.' + PREFIX + '__btn:disabled { opacity: 0.6; cursor: default; }' +
      '.' + PREFIX + '__msg { display: none; font-size: 14px; margin: 12px 0 0; }' +
      '.' + PREFIX + '__msg[data-show] { display: block; }' +
      '.' + PREFIX + '__msg[data-kind="error"] { color: #b91c1c; }' +
      '.' + PREFIX + '__success { padding: 24px 4px; text-align: center; }' +
      '.' + PREFIX + '__success-icon { display: inline-flex; align-items: center; justify-content: center;' +
      ' width: 44px; height: 44px; margin-bottom: 12px; border-radius: 50%; background: var(--hlw-accent);' +
      ' color: #ffffff; font-size: 22px; line-height: 1; }' +
      '.' + PREFIX + '__success-text { font-size: 15px; color: #374151; margin: 0; }' +
      '.' + PREFIX + '__credit { position: absolute; right: 10px; bottom: 6px; font-size: 11px;' +
      ' display: inline-flex; align-items: center; gap: 4px; color: #9ca3af; text-decoration: none; }' +
      '.' + PREFIX + '__credit svg { display: block; }' +
      '.' + PREFIX + '__credit:hover { color: #6b7280; text-decoration: underline; }' +
      '@media (max-width: 480px) { .' + PREFIX + ' { padding: 22px 18px 30px; }' +
      ' .' + PREFIX + '__row { flex-direction: column; gap: 0; } }';
    var style = document.createElement('style');
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }

  function el(tag, className, text) {
    var node = document.createElement(tag);
    if (className) node.className = className;
    if (text != null) node.textContent = text;
    return node;
  }

  function logoSvg() {
    var svgNS = 'http://www.w3.org/2000/svg';
    var svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('viewBox', '0 0 32 32');
    svg.setAttribute('width', '13');
    svg.setAttribute('height', '13');
    svg.setAttribute('aria-hidden', 'true');
    svg.setAttribute('focusable', 'false');
    function circle(cx, cy, r, attrs) {
      var c = document.createElementNS(svgNS, 'circle');
      c.setAttribute('cx', cx);
      c.setAttribute('cy', cy);
      c.setAttribute('r', r);
      for (var key in attrs) c.setAttribute(key, attrs[key]);
      svg.appendChild(c);
    }
    circle(16, 16, 16, { fill: '#0f0a0a' });
    circle(16, 16, 12, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.3 });
    circle(16, 16, 9, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1, opacity: 0.5 });
    circle(16, 16, 6, { fill: 'none', stroke: '#fff5f5', 'stroke-width': 1.2, opacity: 0.9 });
    circle(16, 16, 3, { fill: '#06b6d4' });
    circle(24.5, 7.5, 2, { fill: '#f97316' });
    circle(7, 16, 1.8, { fill: '#f97316' });
    circle(20, 20, 1.5, { fill: '#f97316' });
    return svg;
  }

  function build(script) {
    var action = script.getAttribute('data-hl-action');
    if (!action) {
      console.warn('[haynes lab booking-request-form] data-hl-action is required (URL the form POSTs to). Nothing rendered.');
      return;
    }
    var title = script.getAttribute('data-hl-title') || 'Request a booking';
    var accent = script.getAttribute('data-hl-accent') || '#f97316';
    var successMessage = script.getAttribute('data-hl-success-message') ||
      "Thanks! We'll get back to you shortly to confirm your booking.";

    injectStyles();

    var root = el('div', PREFIX);
    root.style.setProperty('--hlw-accent', accent);

    root.appendChild(el('span', PREFIX + '__title', title));
    root.appendChild(el('p', PREFIX + '__subtitle', 'Pick a preferred date and time — we\u2019ll confirm by phone or email.'));

    var form = document.createElement('form');
    form.noValidate = true;

    function field(labelText, input) {
      var wrap = el('label', PREFIX + '__field');
      wrap.appendChild(el('span', PREFIX + '__label', labelText));
      wrap.appendChild(input);
      return wrap;
    }

    var nameInput = el('input', PREFIX + '__input');
    nameInput.type = 'text';
    nameInput.name = 'name';
    nameInput.autocomplete = 'name';
    nameInput.placeholder = 'Your name';

    var contactInput = el('input', PREFIX + '__input');
    contactInput.type = 'text';
    contactInput.name = 'contact';
    contactInput.autocomplete = 'email';
    contactInput.placeholder = '[email protected] or (555) 123-4567';

    var dateInput = el('input', PREFIX + '__input');
    dateInput.type = 'date';
    dateInput.name = 'date';
    var today = new Date();
    dateInput.min = today.getFullYear() + '-' +
      ('0' + (today.getMonth() + 1)).slice(-2) + '-' +
      ('0' + today.getDate()).slice(-2);

    var windowSelect = el('select', PREFIX + '__input');
    windowSelect.name = 'time_window';
    ['Morning (8am–12pm)', 'Afternoon (12pm–5pm)', 'Evening (5pm–8pm)'].forEach(function (label) {
      var opt = document.createElement('option');
      opt.value = label;
      opt.textContent = label;
      windowSelect.appendChild(opt);
    });

    form.appendChild(field('Name', nameInput));
    form.appendChild(field('Phone or email', contactInput));
    var row = el('div', PREFIX + '__row');
    row.appendChild(field('Preferred date', dateInput));
    row.appendChild(field('Time window', windowSelect));
    form.appendChild(row);

    var btn = el('button', PREFIX + '__btn', 'Request booking');
    btn.type = 'submit';
    form.appendChild(btn);

    var msg = el('p', PREFIX + '__msg');
    msg.setAttribute('role', 'status');
    msg.setAttribute('aria-live', 'polite');

    function showError(text, input) {
      msg.textContent = text;
      msg.setAttribute('data-kind', 'error');
      msg.setAttribute('data-show', '');
      if (input) input.focus();
    }

    form.addEventListener('submit', function (e) {
      e.preventDefault();
      var name = nameInput.value.trim();
      var contact = contactInput.value.trim();
      var date = dateInput.value;
      if (!name) return showError('Please enter your name.', nameInput);
      var isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(contact);
      var isPhone = contact.replace(/[^\d]/g, '').length >= 7;
      if (!isEmail && !isPhone) return showError('Please enter a valid phone number or email address.', contactInput);
      if (!date) return showError('Please pick a preferred date.', dateInput);

      btn.disabled = true;
      var body = new FormData();
      body.append('name', name);
      body.append('contact', contact);
      body.append('date', date);
      body.append('time_window', windowSelect.value);
      fetch(action, { method: 'POST', body: body, headers: { Accept: 'application/json' } })
        .then(function (res) {
          if (res.ok) {
            form.style.display = 'none';
            msg.style.display = 'none';
            var success = el('div', PREFIX + '__success');
            success.setAttribute('role', 'status');
            success.appendChild(el('span', PREFIX + '__success-icon', '✓'));
            success.appendChild(el('p', PREFIX + '__success-text', successMessage));
            root.appendChild(success);
          } else {
            btn.disabled = false;
            showError('Something went wrong — please try again.');
          }
        })
        .catch(function () {
          btn.disabled = false;
          showError('Something went wrong — please try again.');
        });
    });

    root.appendChild(form);
    root.appendChild(msg);

    var credit = el('a', PREFIX + '__credit');
    credit.appendChild(logoSvg());
    credit.appendChild(document.createTextNode('Powered by haynes lab'));
    credit.href = 'https://hayneslab.dev/tools/widgets/booking-request-form';
    credit.target = '_blank';
    credit.rel = 'noopener';
    root.appendChild(credit);

    if (script.parentNode && script.parentNode.nodeName !== 'HEAD') {
      script.insertAdjacentElement('afterend', root);
    } else {
      document.body.appendChild(root);
    }
  }

  function init() {
    var script = findScript();
    if (!script) {
      console.warn('[haynes lab booking-request-form] Could not locate its own script tag. Nothing rendered.');
      return;
    }
    build(script);
  }

  if (document.body) {
    init();
  } else {
    document.addEventListener('DOMContentLoaded', init);
  }
})();

</script>

Configuration

Every option is an attribute on the script tag — the same attributes the customizer above exposes. All attributes are optional unless marked required — defaults apply when omitted.

AttributeDefaultDescription
data-hl-action(required)URL the form POSTs to (Formspree, Basin, or your own endpoint). The widget does not render without it.
data-hl-title"Request a booking"Heading displayed above the form.
data-hl-accent"#f97316"Any CSS color for the submit button, focus outlines, and success icon.
data-hl-success-message"Thanks! We'll get back to you shortly to confirm your booking."Text shown in place of the form after a successful submission.

Frequently asked questions

How do I install the booking request form?

Paste the embed snippet where you want the form to appear — it renders inline at that spot. Set data-hl-action to the URL that should receive submissions (a Formspree/Basin form endpoint or your own API) and you are done.

How do I customize the heading, colors, and success text?

Everything is controlled through data-hl-* attributes on the script tag: data-hl-title for the heading, data-hl-accent for the button and highlight color, and data-hl-success-message for what visitors see after submitting. No CSS editing required.

Does it work on WordPress, Shopify, or Squarespace?

Yes. The widget is a single dependency-free vanilla JS file, so it runs anywhere you can paste a script tag — WordPress (Custom HTML block), Shopify (theme.liquid), Squarespace (code injection), Webflow, or a plain HTML site.

More free widgets

Need a custom widget?

We build custom widgets, integrations, and whole products. Tell us what your site needs and we'll scope it.

Get in Touch