DEV Community

Fazal Shah
Fazal Shah

Posted on

Lottie Animations in SvelteKit: A Complete Guide

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
Enter fullscreen mode Exit fullscreen mode

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.lottie for 75% smaller files
  • Verify everything looks right before integrating

Installation

npm install lottie-web
Enter fullscreen mode Exit fullscreen mode

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;" />
Enter fullscreen mode Exit fullscreen mode
<!-- Usage -->
<LottieAnimation src="/animations/hero.json" width={400} height={400} />
Enter fullscreen mode Exit fullscreen mode

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;" />
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 };
}
Enter fullscreen mode Exit fullscreen mode
<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;" />
Enter fullscreen mode Exit fullscreen mode

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;" />
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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

  1. Always use onMount + dynamic import('lottie-web') — never top-level imports
  2. Call anim.destroy() in onDestroy — components unmount on SvelteKit navigation
  3. Put animation files in static/animations/ and reference by URL
  4. Use SvelteKit's load function to preload critical animation data
  5. Convert to .lottie format at IconKing — 75% smaller files improve Lighthouse scores

Top comments (0)