DEV Community

Fazal Shah
Fazal Shah

Posted on

How to Use Lottie Animations in Svelte (2025 Guide)

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

For .lottie format (smaller files):

npm install @lottiefiles/dotlottie-web
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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

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

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

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

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

  1. Import lottie-web inside onMount — prevents SSR errors in SvelteKit
  2. Always call anim.destroy() in onDestroy — prevents memory leaks
  3. Use bind:this={container} to pass the DOM reference to loadAnimation
  4. Use path for external files or animationData for bundled JSON
  5. Add IntersectionObserver for long-scrolling pages
  6. Convert to .lottie at IconKing for 75% smaller files
  7. Use Canvas renderer when running 5+ animations simultaneously

Top comments (0)