SvelteKit adds SSR and routing on top of Svelte. Lottie requires browser APIs — window, document, and requestAnimationFrame — none of which exist during server rendering. This guide covers every pattern for safely using Lottie in SvelteKit.
The SSR Problem
SvelteKit renders pages on the server. Lottie imports fail during SSR with:
ReferenceError: window is not defined
The solution: always initialize Lottie inside onMount() and use dynamic imports.
Before You Start
Open your animation files in IconKing first:
- Preview colors, timing, and loop behavior
- Convert
.json→.lottiefor 75% smaller files - Verify everything looks right before integrating
Installation
npm install lottie-web
Place animation files in static/animations/ — SvelteKit serves static/ at the root URL.
Basic Component
<!-- src/lib/components/LottieAnimation.svelte -->
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import type { AnimationItem } from 'lottie-web';
export let src: string;
export let width: number = 300;
export let height: number = 300;
export let loop: boolean = true;
export let autoplay: boolean = true;
let container: HTMLDivElement;
let anim: AnimationItem | null = null;
onMount(async () => {
const lottie = (await import('lottie-web')).default;
anim = lottie.loadAnimation({ container, renderer: 'svg', loop, autoplay, path: src });
});
onDestroy(() => { anim?.destroy(); });
export function play() { anim?.play(); }
export function pause() { anim?.pause(); }
export function stop() { anim?.stop(); }
</script>
<div bind:this={container} style="width:{width}px; height:{height}px;" />
<!-- Usage -->
<LottieAnimation src="/animations/hero.json" width={400} height={400} />
dotLottie Format (Smaller Files)
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
export let src: string;
export let width: number = 300;
export let height: number = 300;
let canvas: HTMLCanvasElement;
let dotLottie: any = null;
onMount(async () => {
const { DotLottie } = await import('@lottiefiles/dotlottie-web');
dotLottie = new DotLottie({ canvas, src, loop: true, autoplay: true });
});
onDestroy(() => { dotLottie?.destroy(); });
</script>
<canvas bind:this={canvas} style="width:{width}px; height:{height}px;" />
SvelteKit-Specific: browser Guard
<script lang="ts">
import { onMount } from 'svelte';
import { browser } from '$app/environment';
let container: HTMLDivElement;
onMount(async () => {
if (!browser) return;
const lottie = (await import('lottie-web')).default;
lottie.loadAnimation({ container, renderer: 'svg', loop: true, autoplay: true, path: '/animations/hero.json' });
});
</script>
Route Page with Animation Data
// src/routes/+page.ts
export async function load({ fetch }) {
const animData = await fetch('/animations/hero.json').then(r => r.json());
return { animData };
}
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
export let data;
let container: HTMLDivElement;
let anim: any = null;
onMount(async () => {
const lottie = (await import('lottie-web')).default;
anim = lottie.loadAnimation({
container, renderer: 'svg', loop: true, autoplay: true,
animationData: data.animData,
});
});
onDestroy(() => anim?.destroy());
</script>
<div bind:this={container} style="width:400px; height:400px;" />
IntersectionObserver for Scroll Animations
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
export let src: string;
let container: HTMLDivElement;
let anim: any = null;
let observer: IntersectionObserver | null = null;
onMount(async () => {
const lottie = (await import('lottie-web')).default;
anim = lottie.loadAnimation({ container, renderer: 'svg', loop: true, autoplay: false, path: src });
observer = new IntersectionObserver(
([entry]) => { entry.isIntersecting ? anim?.play() : anim?.pause(); },
{ threshold: 0.1 }
);
observer.observe(container);
});
onDestroy(() => { observer?.disconnect(); anim?.destroy(); });
</script>
<div bind:this={container} style="width:300px; height:300px;" />
Reusable Lottie Store
// src/lib/stores/lottie.ts
import { writable } from 'svelte/store';
function createLottieStore() {
const animations = new Map();
const { subscribe, set } = writable(animations);
return {
subscribe,
register(id: string, anim: any) { animations.set(id, anim); set(animations); },
play(id: string) { animations.get(id)?.play(); },
pause(id: string) { animations.get(id)?.pause(); },
destroy(id: string) { animations.get(id)?.destroy(); animations.delete(id); set(animations); },
};
}
export const lottieStore = createLottieStore();
Performance Checklist for SvelteKit
| Check | Solution |
|---|---|
window is not defined |
Dynamic import('lottie-web') inside onMount
|
Large .json files |
Convert to .lottie at IconKing
|
| Off-screen animations |
IntersectionObserver in onMount
|
| Memory leak on navigation | Always call anim.destroy() in onDestroy
|
Summary
- Always use
onMount+ dynamicimport('lottie-web')— never top-level imports - Call
anim.destroy()inonDestroy— components unmount on SvelteKit navigation - Put animation files in
static/animations/and reference by URL - Use SvelteKit's
loadfunction to preload critical animation data - Convert to
.lottieformat at IconKing — 75% smaller files improve Lighthouse scores
Top comments (0)