One <script> tag. Any host page.
The booking widget drops onto any web page — static HTML, WordPress, Vite, React, Vue, Squarespace — with one script tag. Iframe-isolated, deep-linkable, and instrumented for analytics out of the box.
The integration
Two flavours of the same one-line embed:
<!-- Full catalog -->
<script src="https://app.yourbrand.com/embed.js"></script>
<!-- Direct to a specific product -->
<script
src="https://app.yourbrand.com/embed.js"
data-product="fh_skipper_six"
></script> That’s the entire public contract. No npm package, no framework compatibility shim, no CSS to import, no widget version to track.
What the embed script does
-
Creates an iframe pointing at
https://app.yourbrand.com/embed, with thedata-productvalue passed through as a query parameter so the widget can deep-link straight to one product’s calendar. - Inserts the iframe at the script tag’s location in the DOM, full-width, with a sensible minimum height.
-
Listens for
postMessageevents from the widget and adjusts the iframe height in real time, so there’s no inner scrollbar regardless of how the customer’s screen renders the step they’re on. - Fades the iframe in once the widget signals it’s ready — so customers don’t see a 600-pixel white rectangle while the page loads.
- On checkout, redirects the parent window to Stripe Checkout. (Stripe Checkout blocks iframing for security, so the parent page has to take over — the widget handles this transparently.)
Deep linking from your marketing site
Every product card on your marketing site can have a “Book Now” button that drops customers straight onto the calendar for that product:
<a href="/book/?product=fh_skipper_six">Book Now</a>
On the /book/ page, the widget reads the
product query parameter and skips the catalog step.
Result: from product card to calendar in one click.
Stale or invalid product IDs fall through gracefully to the full catalog instead of erroring — so a stray link in an old email never breaks for the customer.
Analytics events — postMessage hooks for the host page
The widget emits structured events that the host page can listen for, without ever touching widget code. Plumb them into Google Analytics, Plausible, Mixpanel, Segment, or your own logger:
document.addEventListener("lockit:step", (e) => {
// e.detail.step → "catalog" | "calendar" | "time" | "options" | "checkout"
// e.detail.label → human label
gtag("event", "booking_step", e.detail);
});
document.addEventListener("lockit:complete", (e) => {
// e.detail.url → Stripe Checkout URL the parent is about to redirect to
gtag("event", "booking_redirect_to_stripe");
}); lockit:step fires at every funnel transition.
lockit:ready fires when the widget has rendered.
lockit:complete fires when the customer hits pay.
That’s enough to build a complete booking funnel report against
any analytics provider you already use.
Why iframe
The booking widget is always loaded in an iframe, even on the marketing sites operated by the platform itself. Three reasons:
Cross-framework compatibility
The host page can be plain HTML, WordPress, Vite, React, Vue, Svelte, Squarespace, Shopify — doesn’t matter. iframe is the lowest common denominator.
Style isolation
The widget’s CSS doesn’t collide with your marketing site’s. You can rebrand the host page independently and the widget keeps working.
Sandboxing
Even if your marketing site has a security bug, the widget runs in a separate origin with no DOM access. Customer payment details entered in the widget never touch the host page.
The opacity fade — small thing, big difference
Before this fix shipped, the iframe had a 600-pixel minimum height applied immediately at mount, but the widget’s React app took a few hundred milliseconds to render. For that window, customers saw a giant white rectangle in the middle of the page and assumed the widget was broken.
The fix: the iframe starts at zero opacity. Once the widget signals it’s ready, the embed shim fades it in over 150 milliseconds. The white flash is gone. Product switches inside the widget trigger the same fade cycle.
Tiny thing — the kind of polish that you only notice when it’s missing. The platform is full of these.