Building blocksInstallFramework guides
Svelte / SvelteKit
SSR-safe booking page for SvelteKit.
The widget is client-only, so everything happens inside onMount — SSR never
touches window.
<script lang="ts">
import { onMount } from "svelte";
export let apiKey = "mk_your_publishable_key";
const CDN_URL = "https://widgets.medos.one/v2/unified.js";
const CONTAINER_ID = "medos-booking-svelte";
type MedosBookingApi = {
init: (config: Record<string, unknown>) => void;
open: (config?: Record<string, unknown>) => void;
close: () => void;
};
const medos = () =>
(window as unknown as { MedosBooking?: MedosBookingApi }).MedosBooking;
/** Reuses an existing tag so client-side navigation doesn't re-download. */
function load(): Promise<void> {
if (medos()) return Promise.resolve();
const existing = document.querySelector<HTMLScriptElement>(
`script[src="${CDN_URL}"]`,
);
if (existing) {
return new Promise((resolve) =>
existing.addEventListener("load", () => resolve(), { once: true }),
);
}
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = CDN_URL;
script.async = true;
script.onload = () => resolve();
script.onerror = () => reject(new Error("Medos Booking failed to load"));
document.head.appendChild(script);
});
}
onMount(() => {
let cancelled = false;
load()
.then(() => {
if (cancelled) return;
medos()?.init({
apiKey,
mode: "inline",
containerId: CONTAINER_ID,
});
})
.catch((err) => console.error(err));
return () => {
cancelled = true;
// Inline mode has no close() — clear the container so navigating back
// doesn't mount a second widget.
const el = document.getElementById(CONTAINER_ID);
if (el) el.innerHTML = "";
};
});
</script>
<h1>Book an appointment</h1>
<div id={CONTAINER_ID} style="min-height: 600px" />Modal mode
Keep the same onMount block, drop containerId, and switch to
mode: "modal". Then open from a button, guarded in case the bundle is still
loading:
<button on:click={() => medos()?.open()}>Book appointment</button>