How to Add a Calendar to a SvelteKit Site (SSR-Safe)
By SimpleCalendarJS Team
You need to add a calendar to your SvelteKit site. You grab a calendar library, drop it into a +page.svelte file, and it works perfectly in dev — until you deploy. SvelteKit renders every page on the server first, and calendar libraries need the DOM. The result: a ReferenceError: document is not defined in production, or worse, a hydration mismatch that silently breaks your layout. The fix is straightforward once you know the pattern — and SvelteKit actually makes it easier than most frameworks.
The SSR problem with calendar libraries
SvelteKit is not plain Svelte. When a user visits your site, SvelteKit renders the page on the server first, sends the HTML to the browser, then hydrates it with client-side JavaScript. This is what makes SvelteKit sites fast and SEO-friendly — but it breaks any library that touches window, document, or the DOM during initialisation.
Every calendar library does this. FullCalendar, EventCalendar, SVAR Calendar, Schedule-X — they all need a DOM container to render into. On the server, that container doesn't exist.
You have three options:
- Disable SSR entirely with
export const ssr = falsein+page.js— this works but kills SEO and slows initial page load for that route - Dynamic import inside
onMount— the library only loads on the client, but adds a waterfall step - Static import +
{#if browser}guard +use:action — the library tree-shakes out of the server build automatically, and the calendar mounts cleanly on the client
Option 3 is the recommended approach. Here's how.
Adding a calendar to SvelteKit with a use: action
Install SimpleCalendarJS — ~14 KB gzipped, zero dependencies:
npm install simple-calendar-js
Create your calendar component at src/lib/components/Calendar.svelte:
<script> import { browser } from '$app/environment'; import SimpleCalendarJs from 'simple-calendar-js'; import 'simple-calendar-js/dist/simple-calendar-js.min.css'; let calendarInstance; function initCalendar(node) { calendarInstance = new SimpleCalendarJs(node, { defaultView: 'month', locale: 'en-US', enabledViews: ['month', 'week', 'day'], fetchEvents: async (start, end) => { const res = await fetch( `/api/events?from=${start.toISOString()}&to=${end.toISOString()}` ); return res.json(); }, onEventClick: (event) => console.log('Event:', event), onSlotClick: (date) => console.log('Slot:', date), }); return { destroy() { calendarInstance?.destroy(); }, }; } </script> {#if browser} <div use:initCalendar></div> {/if}
Then use it in any +page.svelte:
<script> import Calendar from '$lib/components/Calendar.svelte'; </script> <h1>My Schedule</h1> <Calendar />
Why this works
The {#if browser} block ensures the <div> only renders on the client. On the server, SvelteKit skips the block entirely — no DOM element means no use: action fires, and the calendar import is tree-shaken out of the server bundle because it's only referenced inside client-only code. No dynamic imports, no onMount waterfall, no special config.
The use:initCalendar action provides the exact lifecycle SvelteKit needs: it receives the DOM node when Svelte mounts the element, and calls destroy() when the component unmounts — whether the user navigates away via SvelteKit's client-side router or the component is conditionally removed.
Wiring the calendar to SvelteKit's load function
SvelteKit's load function runs on both server and client, making it the right place to fetch event data. Pass the data to your calendar component as a prop:
// src/routes/schedule/+page.js export async function load({ fetch }) { const res = await fetch('/api/events'); const events = await res.json(); return { events }; }
<!-- src/routes/schedule/+page.svelte --> <script> import Calendar from '$lib/components/Calendar.svelte'; let { data } = $props(); </script> <Calendar initialEvents={data.events} />
Then update Calendar.svelte to accept initial events:
<script> import { browser } from '$app/environment'; import SimpleCalendarJs from 'simple-calendar-js'; import 'simple-calendar-js/dist/simple-calendar-js.min.css'; let { initialEvents = [] } = $props(); function initCalendar(node) { const calendar = new SimpleCalendarJs(node, { defaultView: 'month', locale: 'en-US', enabledViews: ['month', 'week', 'day'], events: initialEvents, fetchEvents: async (start, end) => { const res = await fetch( `/api/events?from=${start.toISOString()}&to=${end.toISOString()}` ); return res.json(); }, onEventClick: (event) => console.log('Event:', event), }); return { destroy() { calendar?.destroy(); } }; } </script> {#if browser} <div use:initCalendar></div> {/if}
The events load in +page.js (which runs on the server for the first visit), and the calendar renders them instantly on hydration — no loading spinner, no layout shift.
The SvelteKit calendar landscape
Here's what's available for SvelteKit specifically, with the SSR-readiness of each option:
| Library | Gzipped Size | SSR-Safe? | Dependencies | Views |
|---|---|---|---|---|
| EventCalendar | ~35 KB brotli | Needs guard | None | Day, week, month, resource |
| SVAR Calendar | ~40 KB | Built-in SSR support | Svelte 5 | Day, week, month |
| Schedule-X | ~30 KB+ | Needs guard | Preact, @preact/signals, temporal-polyfill | Day, week, month |
| svelte-fullcalendar | ~50 KB+ | Reported issues | FullCalendar core + plugins | Depends on plugins |
| SimpleCalendarJS | ~14 KB | Works with {#if browser} | None | Month, week, day |
SVAR Calendar is the only option with documented built-in SSR handling for SvelteKit, but at ~40 KB gzipped and a Svelte 5 requirement. Schedule-X pulls in Preact and @preact/signals as peer dependencies — a competing framework's runtime inside your Svelte project. svelte-fullcalendar wraps FullCalendar and has documented import ordering issues in SvelteKit builds.
Theming for dark mode in SvelteKit
SimpleCalendarJS uses CSS custom properties, which work perfectly with SvelteKit's scoped styles:
<div use:initCalendar class="my-calendar"></div> <style> .my-calendar :global(.uc-calendar) { --cal-primary: #ff3e00; --cal-primary-dark: #d63600; --cal-today-bg: #fff0eb; --cal-font-size: 14px; } @media (prefers-color-scheme: dark) { .my-calendar :global(.uc-calendar) { --cal-bg: #1a1a2e; --cal-text: #e2e8f0; --cal-border: #2d2d44; --cal-today-bg: #2d2d44; } } </style>
Four CSS variables and the calendar matches your SvelteKit site's design system — including automatic dark mode support.
Common mistakes to avoid
Don't disable SSR for the whole page. Adding export const ssr = false to +page.js works, but that page loses server-side rendering entirely. Search engines won't index the content that loads after JavaScript runs, and your users see a blank page until the bundle downloads. Use the {#if browser} guard instead — the rest of your page content still gets server-rendered.
Don't use onMount with a static import for side-effectful libraries. If the calendar library executes code at import time (accessing window or document at the module level), a static import will crash the SSR build even if you only call the constructor inside onMount. Use a dynamic import inside onMount as a fallback:
<script> import { onMount } from 'svelte'; let container; onMount(async () => { const { default: SimpleCalendarJs } = await import('simple-calendar-js'); await import('simple-calendar-js/dist/simple-calendar-js.min.css'); const calendar = new SimpleCalendarJs(container, { defaultView: 'month', enabledViews: ['month', 'week', 'day'], }); return () => calendar?.destroy(); }); </script> <div bind:this={container}></div>
SimpleCalendarJS is side-effect free at import time, so the static import with {#if browser} approach works cleanly. But the onMount pattern above is a safe fallback for any library.
Summary
- SvelteKit renders pages on the server first — every calendar library that touches the DOM needs an SSR guard to work correctly
- Don't disable SSR entirely — use
{#if browser}from$app/environmentto guard only the calendar container, keeping the rest of your page SEO-friendly - Svelte's
use:action +{#if browser}is the cleanest integration pattern — no dynamic imports, noonMountboilerplate, automatic cleanup on navigation - Wire event data through SvelteKit's
loadfunction to avoid loading spinners — events arrive with the server-rendered page - SimpleCalendarJS delivers month, week, and day views in ~14 KB gzipped with a single
use:action — SSR-safe, zero dependencies, and 3.5x lighter than the nearest alternative
Sources & Further Reading
Research & References
- SvelteKit Page Options — ssr, csr, prerender — svelte.dev
- How do I use a client-side only library in SvelteKit — SvelteKit FAQ
- Svelte use: directive documentation — svelte.dev
- Built-in support for importing client-only components — SvelteKit Discussion #10439
- How to Add a Client Side Only Library to SvelteKit — banjocode
- EventCalendar — Full-sized drag & drop event calendar — GitHub
- SVAR Svelte Calendar — Setup Documentation
- Schedule-X Svelte component — schedule-x.dev
- SvelteKit & FullCalendar import issues — GitHub #3116
Image Credits
- Cover: Computer Coding on a Computer Screen — Pexels
All images free to use under the Pexels License.
Frequently Asked Questions
How do I add a calendar to a SvelteKit site?
Install a calendar library via npm, then integrate it using Svelte's use: action directive wrapped in an {#if browser} guard. Import { browser } from '$app/environment' to prevent the calendar from initialising during server-side rendering. The use: action provides a DOM node on mount and a destroy callback for cleanup.
Why does my calendar break in SvelteKit but work in plain Svelte?
SvelteKit renders every page on the server first by default. Calendar libraries depend on browser APIs like window and document that don't exist during SSR — so they throw errors or produce hydration mismatches. Plain Svelte only runs client-side, which is why the same code works there. Guard your calendar code with the browser check from $app/environment or use onMount.
Should I disable SSR for my entire SvelteKit app to use a calendar?
No. Adding export const ssr = false to your root +layout.js disables SSR for every page, which hurts SEO and initial load performance. Instead, guard only the calendar component with {#if browser} or use onMount — the rest of your page still gets server-rendered and indexed by search engines.
What is the best calendar library for SvelteKit?
For a Svelte-native event calendar with drag-and-drop, EventCalendar (~35 KB brotli) is the most popular option. SVAR Calendar (~40 KB gzipped) is built for Svelte 5. For the lightest footprint, SimpleCalendarJS (~14 KB gzipped) works in any SvelteKit site via a use: action and ships month, week, and day views with zero dependencies.
Can I use FullCalendar with SvelteKit?
Yes, but it requires extra work. The svelte-fullcalendar wrapper provides a Svelte component, but SvelteKit users have reported import ordering issues during SSR builds. You also need @fullcalendar/core plus separate view plugins, adding ~50 KB+ gzipped to your bundle. Simpler alternatives avoid these SSR complications entirely.
What is the difference between onMount and {#if browser} for client-only code in SvelteKit?
Both prevent code from running on the server. onMount fires once after the component mounts to the DOM — ideal for initialising a library. {#if browser} conditionally renders a DOM element only on the client — useful for ensuring the container element exists before a use: action runs. For calendar integration, combining both patterns gives you the most reliable SSR-safe setup.
Add a calendar to your app today
Free for personal projects. $49/year or $199 lifetime per commercial project.
