MonkeyLoader All articles
Performance

Leave the Diary Blank Until They Click: A Booking Widget That Earns Its Place

MonkeyLoader
Leave the Diary Blank Until They Click: A Booking Widget That Earns Its Place

Photo: Lewis Clarke, CC BY-SA 2.0, via Wikimedia Commons

Most UK clinic, salon and leisure-centre sites have the same expensive habit: a third-party booking iframe parked on every page. It sits there on the homepage, the contact page, the parking directions page, quietly pulling in scripts, styles and cookies for people who only wanted to check opening hours. On a good connection it is merely wasteful. On a wobbly one it is the reason the page feels broken before it has started.

The fix is not to remove booking. It is to stop treating every page view as a booking attempt. Put an empty slot where the widget would go, make the slot look and behave like a real call to action, and load the third-party diary only when someone shows clear intent. Keep a phone number visible the whole time. That last part matters more than any script optimisation.

Start with an honest placeholder

The placeholder is not a grey box with a spinner. It is a small, useful panel: a heading, a short line explaining what happens next, a button and a telephone link. Reserve enough height so the page does not jump when the real widget arrives.

<section class='booking-slot' data-booker-src='https://booker.example.co.uk/widget?clinic=thameside'>
  <h2>Book an appointment</h2>
  <p>Open the online diary for Thameside Clinic. It usually takes a few seconds to load.</p>
  <button type='button' class='booking-load'>Open the online diary</button>
  <p class='booking-fallback'>Prefer to speak to someone? <a href='tel:+441234567890'>Call 01234 567 890</a>.</p>
  <noscript>
    <p>Online booking needs JavaScript. Please call <a href='tel:+441234567890'>01234 567 890</a>.</p>
  </noscript>
</section>

Give .booking-slot a sensible min-height in CSS. If the widget is 640px tall on mobile, reserve roughly that. A little empty space is cheaper than a layout shift that makes someone tap the wrong thing.

Make the trigger obvious

A click is the simplest signal. Someone who taps “Open the online diary” has told you what they want. You do not need to guess from scroll depth, mouse movement or time on page.

For services that ask for a postcode before showing availability, use a small form instead. Let the visitor enter their postcode and press a button. Only then load the widget, and pass the postcode through if the booking provider supports it. If the provider does not support prefill, still load on submit and let the widget ask for the postcode itself. The point is to avoid loading the diary for every person browsing the site.

Do not load on hover. Hover is not intent, especially on touch screens, and it creates a race between the pointer and the network. Do not load on page idle either unless the slot is high on the page and you have measured that the cost is acceptable. For most UK clinic and leisure sites, click or submit is enough.

The loader script

The script below is deliberately small. It finds each booking slot, waits for a click or form submit, swaps in an iframe once, and leaves the phone fallback in place. It also handles the case where the widget fails to load.

<script>
(function () {
  document.querySelectorAll('.booking-slot').forEach(function (slot) {
    var button = slot.querySelector('.booking-load');
    var fallback = slot.querySelector('.booking-fallback');
    var src = slot.getAttribute('data-booker-src');
    var loaded = false;

    function loadBooker() {
      if (loaded || !src) return;
      loaded = true;

      if (button) button.disabled = true;

      var frame = document.createElement('iframe');
      frame.src = src;
      frame.title = 'Online booking diary';
      frame.loading = 'lazy';
      frame.setAttribute('allow', 'payment; clipboard-write');
      frame.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin');
      frame.className = 'booking-frame';

      frame.addEventListener('load', function () {
        slot.classList.add('booking-ready');
        if (button) button.remove();
      });

      frame.addEventListener('error', function () {
        loaded = false;
        if (button) button.disabled = false;
        if (fallback) fallback.hidden = false;
      });

      slot.appendChild(frame);
    }

    if (button) button.addEventListener('click', loadBooker);

    var form = slot.querySelector('.booking-postcode-form');
    if (form) {
      form.addEventListener('submit', function (event) {
        event.preventDefault();
        var input = form.querySelector('input[name="postcode"]');
        var postcode = input ? input.value.trim() : '';
        if (postcode) {
          var url = new URL(src);
          url.searchParams.set('postcode', postcode);
          src = url.toString();
        }
        loadBooker();
      });
    }
  });
})();
</script>

If the provider gives you a fixed iframe snippet with its own attributes, keep the important ones: title, allow, referrerpolicy and a sensible loading value. Do not copy a 300-line vendor loader into the head of every page just to show a diary. You only need the iframe when the slot is activated.

Keep the phone number, not just as a nicety

A visible telephone link is the fallback for everyone whose scripts are blocked, whose connection drops, whose browser extension eats third-party frames, or who simply does not want to use an online diary. It is also useful for older patients, carers and anyone booking on behalf of someone else.

Put the number in the placeholder before any widget loads, and keep it visible afterwards if the design allows. If the widget replaces the whole panel, add a small “Call us” link beside the diary heading. Do not hide the number behind a “contact” page. The empty slot should still be useful when the slot stays empty.

Check consent before you wire it up

Booking widgets often set cookies or load analytics from the provider’s domain. If yours does, treat the click as the moment to check consent, not as automatic consent. A consent-aware loader can load the iframe only after the visitor has agreed to the relevant category, or use a provider that offers a cookieless mode. This is especially important for NHS-adjacent services and anything handling health information. The phone link should remain available whether consent is given or not.

Test the boring failure cases

Throttle the connection to something bleak, then block the booking domain and try again. The placeholder should stay usable, the button should not spin forever, and the phone number should still be tappable. Tab through with a keyboard: the trigger, the postcode field and the telephone link should all be reachable in a sensible order. Check that the page does not jump when the iframe appears. On a phone, check that the iframe does not trap scrolling in a way that makes the rest of the page unreachable.

Finally, measure what changed. Compare third-party requests, main-thread work and Largest Contentful Paint before and after on a typical mobile connection. You are not chasing a perfect score. You are removing a cost that most visitors never asked to pay.

The booking diary still matters. It just does not need to be loaded for someone reading about parking.

All Articles

Related Articles

Assume the Signal Will Drop: A Loader Built for the Morning Commute

Assume the Signal Will Drop: A Loader Built for the Morning Commute

The Font That Waits Its Turn: Public-Sector Type Without the Jolt

The Font That Waits Its Turn: Public-Sector Type Without the Jolt

Queue Fairly or Fold: Backpressure Lessons from a UK Ticket Drop

Queue Fairly or Fold: Backpressure Lessons from a UK Ticket Drop