Are you looking to seamlessly integrate powerful scheduling capabilities into your SvelteKit application without the hassle of complex external scripts? Cal.com provides a robust solution, and with Svelte 5's innovative runes, embedding its features is not just straightforward, but elegantly Svelte-native. This blog post will guide you through integrating Cal.com's inline, floating popup, and element-click embeds into a SvelteKit application, leveraging Svelte 5's new reactivity system, known as runes, for a cleaner, more performant, and truly Svelte-native implementation.
Our Modular Approach
To keep our SvelteKit project organized and maintainable, we'll adopt a modular approach. This involves creating a dedicated utility module for managing the Cal.com script and two Svelte components to handle the different embed types:
- cal.js: A utility module responsible for loading the Cal.com embed script and providing functions to initialize namespaces and call Cal.com's API methods.
- Cal.svelte: A versatile Svelte component that handles both inline and floatingButton embed types. It also takes care of initializing namespaces for elementClick embeds.
This solution ensures modularity, reusability, and alignment with Svelte 5’s runes-based reactivity.
Core Components Explained
Let's briefly look at the role of each piece of the solution:
1. cal.js - The Script Manager
This JavaScript module acts as the central point for managing the Cal.com embed script. Its primary functions are:
-
initCal(): Ensures the Cal.com embed script (https://app.cal.com/embed/embed.js) is loaded into the document's
<head>
only once, preventing duplicate script injections. - initNamespace(namespace, options): Initializes a specific Cal.com namespace (e.g., '15min') with given options, such as the origin URL. This function ensures that a namespace is only initialized if it hasn't been already.
- callNamespaceMethod(namespace, method, ...args): Provides a safe way to call methods (like "inline", "floatingButton", or "ui") on a specific Cal.com namespace, with built-in checks to ensure the namespace is initialized.
// @ts-nocheck Can't determine types for Cal
const initializedNamespaces = new Set();
export function initCal() {
// if (window.Cal) return; // Keep this commented or remove if Cal can be re-initialized safely
(function (C, A, L) {
let p = function (a, ar) {
a.q.push(ar);
};
let d = C.document;
C.Cal =
C.Cal ||
function () {
let cal = C.Cal;
let ar = arguments;
if (!cal.loaded) {
cal.ns = {};
cal.q = cal.q || [];
d.head.appendChild(d.createElement('script')).src = A;
cal.loaded = true;
}
if (ar[0] === L) {
const api = function () {
p(api, arguments);
};
const namespace = ar[1];
api.q = api.q || [];
if (typeof namespace === 'string') {
cal.ns[namespace] = cal.ns[namespace] || api;
p(cal.ns[namespace], ar);
p(cal, ['initNamespace', namespace]);
} else p(cal, ar);
return;
}
p(cal, ar);
};
})(window, 'https://app.cal.com/embed/embed.js', 'init');
}
export function initNamespace(namespace, options = { origin: 'https://cal.com' }) {
initCal(); // Ensures Cal global and script loading logic is in place
const namespaceKey = `${namespace}-${options.origin}`;
if (!initializedNamespaces.has(namespaceKey)) {
window.Cal('init', namespace, options);
initializedNamespaces.add(namespaceKey);
}
// The rest of the logic for ensuring Cal.ns[namespace] exists is handled by initCal's core logic
}
export function callNamespaceMethod(namespace, method, ...args) {
if (window.Cal && window.Cal.ns && window.Cal.ns[namespace]) {
window.Cal.ns[namespace](method, ...args);
} else {
// Queue it if the namespace isn't ready yet, or warn.
// The original Cal script has a built-in queue, so this might also be handled if `initNamespace`
// has been called and `embed.js` is just loading.
// For robustness, you might consider adding a local queue here too or relying on Cal's internal one.
// console.warn(`Namespace ${namespace} not fully initialized or Cal.com script not loaded yet. Attempting to queue call.`);
// Fallback to global queue if namespace specific queue isn't set up by embed.js yet
window.Cal(namespace, method, ...args);
}
}
2. Cal.svelte - The Main Embed Wrapper
This Svelte 5 component uses Svelte 5 runes and handles all embed types based on the embedType prop. It renders a container for inline embeds and initializes the Cal API for all types.
- It accepts props like
embedType
('inline', 'floatingButton', or null for element-click),namespace
,calLink
,config
,ui
settings, andorigin
. - Using Svelte's
onMount
lifecycle hook, it initializes the Cal.com namespace and then calls the appropriate embed method ("inline" or "floatingButton") based on theembedType
prop. - For inline embeds, it renders a
div
element that serves as the container for the embedded calendar, binding it to a$state
variable for direct DOM manipulation. For other embed types, it renders nothing directly but handles the script initialization.
<!--
@component
A Svelte component to embed Cal.com scheduling links.
It wraps the Cal.com embed.js script and provides different embed types (inline, floating button).
It handles the initialization and API calls to the Cal.com script based on the provided props.
@param { 'inline' | 'floatingButton' | null } [embedType=null] - The type of Cal.com embed. Use 'inline' to embed directly in a div, 'floatingButton' for a popup button, or null for element-click (handled by CalLink.svelte).
@param { string } [namespace='15min'] - The namespace for the Cal.com embed instance. Useful if you have multiple embeds on the page.
@param { string } calLink - The Cal.com scheduling link (e.g., 'your-username/event-type'). Required for the embed to function.
@param { Record<string, any> } [config={ layout: 'month_view' }] - Configuration options for the Cal.com embed.
@param { Record<string, any> | null } [ui={ hideEventTypeDetails: false, layout: 'month_view' }] - UI customization options for the Cal.com embed.
@param { string } [origin='https://cal.com'] - The origin URL for the Cal.com embed.
@param { string } [class=''] - CSS class to apply to the container element (for inline type).
-->
<script lang="ts">
import { initNamespace, callNamespaceMethod } from '$lib/cal.js';
import { onMount } from 'svelte';
let {
embedType = null as 'inline' | 'floatingButton' | null,
namespace = '15min',
calLink = '',
config = { layout: 'month_view' },
ui = { hideEventTypeDetails: false, layout: 'month_view' } as Record<string, any> | null,
origin = 'https://cal.com',
class: className = ''
} = $props();
// Generate a unique ID for the inline container based on the namespace
// This assumes namespaces are unique if multiple inline embeds are on the page.
let divId: string = $derived(`cal-inline-container-${namespace}`);
onMount(() => {
// Initialize the namespace (idempotent thanks to changes in cal.js)
initNamespace(namespace, { origin });
if (embedType === 'inline') {
// Ensure the div is actually in the DOM before trying to use it.
// Svelte's rendering is synchronous with effect execution unless using $effect.pre or $effect.layout
// For $effect, the DOM should be updated.
// A small delay might be safer if Cal.com's script runs too quickly,
// but ideally, its queuing handles elements not immediately present.
// The selector method is generally more robust.
callNamespaceMethod(namespace, 'inline', {
elementOrSelector: `#${divId}`, // Use the selector string
config,
calLink
});
} else if (embedType === 'floatingButton') {
callNamespaceMethod(namespace, 'floatingButton', { calLink, config });
}
// Configure UI (if ui prop is provided)
// This will be called after 'inline' or 'floatingButton'
if (ui) {
callNamespaceMethod(namespace, 'ui', ui);
}
});
</script>
{#if embedType === 'inline'}
<div id={divId} class={className} style="width:100%;height:100%;overflow:scroll">
</div>
{/if}
Practical Usage in Your SvelteKit App
Here's how you can easily integrate these components into your SvelteKit pages:
1. Inline Embed
Here's how to implement an inline embed in your SvelteKit page:
<script>
import Cal from '$lib/Cal.svelte';
</script>
<div style="height: 600px; width: 100%;">
<Cal embedType="inline" namespace="my-meeting" calLink="your-username/15min" />
</div>
Note: Ensure the parent container of the Cal component for inline embeds has defined dimensions for the calendar to render correctly.
2. Floating Popup Embed
The floating popup embed adds a button that, when clicked, opens the calendar in a popup.
<script>
import Cal from '$lib/Cal.svelte';
</script>
<Cal embedType="floatingButton" namespace="15min" calLink="your-username/15min" />
3. Element-Click Embed
The element-click embed allows any element to trigger the calendar popup. With the updated Cal.svelte component, you can now directly use a standard HTML element with specific data-cal-*
attributes to achieve this. The Cal component still needs to be present on the page to initialize the Cal.com script and namespace.
<script>
import Cal from '$lib/Cal.svelte';
</script>
<Cal namespace="15min" />
<button
data-cal-link="your-username/15min"
data-cal-namespace="15min"
class="px-6 py-3 bg-blue-600 text-white rounded-lg shadow-md hover:bg-blue-700 transition-colors"
>
Book a 15min Meeting
</button>
How It Works
Utility Module (cal.js)
- initCal: Loads the Cal.com script only once, ensuring no duplicate script injections.
- initNamespace: Initializes a namespace (e.g., "15min") with options like origin, avoiding redundant initializations.
- callNamespaceMethod: Calls methods on the namespace (e.g., "inline", "floatingButton", "ui") with safety checks.
Main Embed Component (Cal.svelte)
-
Props: Uses
$props()
to defineembedType
,namespace
,calLink
,config
,ui
, andorigin
. -
Lifecycle: On mount, it initializes the namespace and calls the appropriate method based on
embedType
. -
Rendering: For inline, it renders a
<div>
and binds it to container for the calendar; for other types, it renders nothing.
Customization
You can customize the embeds by passing props:
- Change the Namespace:
<Cal embedType="inline" namespace="30min" calLink="your-username/30min" />
- Adjust UI Settings:
<Cal
embedType="floatingButton"
namespace="15min"
calLink="your-username/15min"
ui={{ hideEventTypeDetails: true, layout: 'week_view' }}
/>
- Multiple Embeds: Use multiple instances with different namespaces:
<Cal embedType="inline" namespace="15min" calLink="your-username/15min" />
<Cal embedType="floatingButton" namespace="30min" calLink="your-username/30min" />
Key Benefits of This Approach
-
Svelte 5 Runes Compliance: Fully embraces the new
$props()
and$state()
runes, ensuring compatibility and leveraging Svelte 5's cutting-edge reactivity system for optimal performance and developer experience. -
No Raw Scripts: Eliminates the need to scatter
<script>
tags directly in your Svelte components by encapsulating script loading and management within thecal.js
utility, leading to cleaner, more maintainable, and secure code. - Modular and Reusable: Each component serves a specific purpose, making your codebase highly modular and the components easily reusable across your application.
- Flexible and Customizable: Supports all Cal.com embed types with extensive customization options via component props.
- Improved Developer Experience: Provides a Svelte-native way to interact with the Cal.com API, making integration feel like a natural extension of your SvelteKit app.
Ready to streamline your application's scheduling? Dive into the code examples and start building your integrated Cal.com experience today! For more advanced configurations, refer to the official Cal.com embed documentation.
Top comments (5)
Pretty cool seeing how smooth this setup looks now - I remember when stuff like this was way messier.
I honestly hadn't used it before, but I had to migrate a codebase from svelte 4 to svelte 5 for a client, and this was one of the things they required changed, so I found a cleaner method to implement it in svelte 5.
Growth like this is always nice to see. Kinda makes me wonder - what keeps this momentum actually lasting? Like, beyond the first few releases.
In my opinion, the Svelte crew enjoys what they are doing and what it is developing into, as opposed to just following orders from superiors or doing it because they were instructed not to.
Impressed with this app!!