Svelte's reactive model makes it a natural fit for Lottie animations. This guide covers everything from setup to programmatic control — including SSR gotchas with SvelteKit and the cleanest patterns for interactive animations.
Step 0: Preview Your Animation First
Before writing any code, drop your .json or .lottie file into IconKing:
- See exactly how it renders in a browser environment
- Edit colors to match your design system
- Convert
.json→.lottiefor 75% smaller file size - Catch broken layers or unsupported effects before runtime
No account needed.
Installation
Svelte doesn't have a first-party Lottie wrapper, but lottie-web works perfectly with Svelte's onMount lifecycle:
npm install lottie-web
For .lottie format (smaller files):
npm install @lottiefiles/dotlottie-web
Basic Setup with lottie-web
<script>
import { onMount, onDestroy } from 'svelte';
import lottie from 'lottie-web';
let container;
let anim;
onMount(() => {
anim = lottie.loadAnimation({
container,
renderer: 'svg',
loop: true,
autoplay: true,
path: '/animations/loading.json'
});
});
onDestroy(() => {
anim?.destroy();
});
</script>
<div bind:this={container} style="width: 200px; height: 200px;" />
The bind:this directive gives you a reference to the DOM element. Always destroy the animation in onDestroy to prevent memory leaks.
Using Animation Data Directly (No File Request)
<script>
import { onMount, onDestroy } from 'svelte';
import lottie from 'lottie-web';
import animationData from '$lib/animations/loading.json';
let container;
let anim;
onMount(() => {
anim = lottie.loadAnimation({
container,
renderer: 'svg',
loop: true,
autoplay: true,
animationData // pass JSON object directly — no network request
});
});
onDestroy(() => anim?.destroy());
</script>
<div bind:this={container} />
Programmatic Playback Control
<script>
import { onMount, onDestroy } from 'svelte';
import lottie from 'lottie-web';
import animData from '$lib/animations/success.json';
let container;
let anim;
onMount(() => {
anim = lottie.loadAnimation({
container,
renderer: 'svg',
loop: false,
autoplay: false,
animationData: animData
});
});
onDestroy(() => anim?.destroy());
</script>
<div bind:this={container} style="width: 120px; height: 120px;" />
<div>
<button on:click={() => anim?.play()}>Play</button>
<button on:click={() => anim?.pause()}>Pause</button>
<button on:click={() => anim?.stop()}>Stop</button>
<button on:click={() => anim?.setSpeed(2)}>2x Speed</button>
<button on:click={() => anim?.setDirection(-1)}>Reverse</button>
</div>
Reusable Lottie Component
Create a reusable wrapper so you don't repeat the onMount/onDestroy boilerplate:
<!-- src/lib/components/LottiePlayer.svelte -->
<script>
import { onMount, onDestroy } from 'svelte';
import lottie from 'lottie-web';
export let animationData = null;
export let path = null;
export let loop = true;
export let autoplay = true;
export let renderer = 'svg';
export let width = 200;
export let height = 200;
let container;
let anim;
export function play() { anim?.play(); }
export function pause() { anim?.pause(); }
export function stop() { anim?.stop(); }
export function setSpeed(s) { anim?.setSpeed(s); }
onMount(() => {
anim = lottie.loadAnimation({
container,
renderer,
loop,
autoplay,
...(animationData ? { animationData } : { path })
});
return () => anim?.destroy();
});
</script>
<div
bind:this={container}
style="width: {width}px; height: {height}px;"
/>
SvelteKit: SSR Gotcha
In SvelteKit, lottie-web accesses browser APIs. If you import it at the top level of a server-rendered component, you'll get:
ReferenceError: document is not defined
The fix is to import inside onMount (which only runs in the browser):
<script>
import { onMount, onDestroy } from 'svelte';
let container;
let anim;
onMount(async () => {
// Dynamic import — runs browser-side only
const lottie = (await import('lottie-web')).default;
anim = lottie.loadAnimation({
container,
renderer: 'svg',
loop: true,
autoplay: true,
path: '/animations/loading.json'
});
});
onDestroy(() => anim?.destroy());
</script>
<div bind:this={container} />
Using dotLottie Format
.lottie files are ~75% smaller than .json. Use @lottiefiles/dotlottie-web for these:
<script>
import { onMount, onDestroy } from 'svelte';
let canvas;
let dotLottie;
onMount(async () => {
const { DotLottie } = await import('@lottiefiles/dotlottie-web');
dotLottie = new DotLottie({
canvas,
src: '/animations/loading.lottie',
loop: true,
autoplay: true,
});
});
onDestroy(() => dotLottie?.destroy());
</script>
<!-- dotlottie-web requires a canvas element -->
<canvas bind:this={canvas} width="200" height="200" />
Convert your .json to .lottie at IconKing — drop the file, click convert, download.
Pause Off-Screen with IntersectionObserver
<script>
import { onMount, onDestroy } from 'svelte';
import lottie from 'lottie-web';
import animData from '$lib/animations/hero.json';
let container;
let anim;
let observer;
onMount(() => {
anim = lottie.loadAnimation({
container,
renderer: 'svg',
loop: true,
autoplay: false,
animationData: animData
});
observer = new IntersectionObserver(([entry]) => {
entry.isIntersecting ? anim.play() : anim.pause();
});
observer.observe(container);
});
onDestroy(() => {
anim?.destroy();
observer?.disconnect();
});
</script>
<div bind:this={container} style="width: 300px; height: 300px;" />
Performance Tips
1. Always destroy on component teardown
Svelte components mount and unmount frequently during navigation. Failing to call anim.destroy() in onDestroy causes memory leaks that compound over time.
2. Use Canvas renderer for many animations
If you're rendering a list of items with animations (carousels, card grids), switch to renderer: 'canvas' — significantly faster than SVG for 5+ simultaneous animations.
3. Use .lottie format
~75% smaller = faster LCP, better Lighthouse scores. Convert at IconKing.
Common Issues
document is not defined in SvelteKit
Import lottie-web inside onMount using dynamic import(), not at the top of the <script> block.
Animation plays once and stops
Check your loop option. Many Lottie wrappers default to loop: false. Set it explicitly.
Colors look wrong
Open the file in IconKing to see the authoritative browser render. If it looks wrong there, the issue is in the After Effects export.
Memory leak on page navigation
Ensure anim.destroy() is called inside onDestroy. In SvelteKit with +page.svelte files, components are destroyed on navigation — always clean up.
Summary
- Import
lottie-webinsideonMount— prevents SSR errors in SvelteKit - Always call
anim.destroy()inonDestroy— prevents memory leaks - Use
bind:this={container}to pass the DOM reference toloadAnimation - Use
pathfor external files oranimationDatafor bundled JSON - Add
IntersectionObserverfor long-scrolling pages - Convert to
.lottieat IconKing for 75% smaller files - Use Canvas renderer when running 5+ animations simultaneously
Top comments (0)