DEV Community

Fazal Shah
Fazal Shah

Posted on

Lottie Animations in Astro.js: A Complete Guide

Astro's islands architecture makes Lottie integration unique: animations live in client-side islands while the rest of the page stays static HTML. This guide covers every pattern — from basic client components to scroll-triggered animations and the right client:* directive for each use case.


The Astro + Lottie Mental Model

Astro renders everything as static HTML by default. Lottie requires JavaScript. The solution: wrap Lottie in a framework component (React, Vue, Svelte, or vanilla) and load it as an island with client:visible or client:load.

Best directive for Lottie:

  • client:visible — load and hydrate only when the animation enters the viewport (recommended for most animations)
  • client:load — load immediately on page load (use for above-fold hero animations only)
  • client:idle — load during browser idle time (good for decorative animations)

Before You Start

Open your animation files in IconKing first:

  • Preview colors, timing, and layers
  • Convert .json → .lottie for 75% smaller file size
  • Verify the animation renders correctly before deploying

Setup

Install your preferred renderer. For React islands:

npx astro add react
npm install lottie-react
Enter fullscreen mode Exit fullscreen mode

For Vue islands:

npx astro add vue
npm install vue3-lottie
Enter fullscreen mode Exit fullscreen mode

For dotLottie (recommended for .lottie format):

npx astro add react
npm install @lottiefiles/dotlottie-react
Enter fullscreen mode Exit fullscreen mode

Place animation files in public/animations/ — Astro serves the public/ directory statically, making files available at /animations/file.json.


React Island (lottie-react)

// src/components/LottieAnimation.jsx
import Lottie from 'lottie-react';

export default function LottieAnimation({ src, width = 200, height = 200 }) {
  return (
    <div style={{ width, height }}>
      <Lottie
        animationData={src}
        loop
        autoplay
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
---
// src/pages/index.astro
import LottieAnimation from '../components/LottieAnimation.jsx';
import heroAnim from '../animations/hero.json';
---

<html>
  <body>
    <!-- client:load for above-fold hero animation -->
    <LottieAnimation client:load src={heroAnim} width={400} height={400} />
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Note: Importing JSON directly bundles it into the JS. For large files, use src="/animations/hero.json" and fetch at runtime instead (see lazy loading section below).


dotLottie React Island (.lottie format)

The .lottie format (75% smaller) works well in Astro since files live in public/:

// src/components/DotLottieAnim.jsx
import { DotLottieReact } from '@lottiefiles/dotlottie-react';

export default function DotLottieAnim({ src, width = 200, height = 200, loop = true }) {
  return (
    <DotLottieReact
      src={src}
      loop={loop}
      autoplay
      style={{ width, height }}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode
---
import DotLottieAnim from '../components/DotLottieAnim.jsx';
---

<!-- client:visible loads only when scrolled into view -->
<DotLottieAnim
  client:visible
  src="/animations/feature.lottie"
  width={300}
  height={300}
/>
Enter fullscreen mode Exit fullscreen mode

Place feature.lottie in public/animations/.


Vue Island (vue3-lottie)

<!-- src/components/LottieVue.vue -->
<script setup>
import Vue3Lottie from 'vue3-lottie'
defineProps({ animationData: Object, width: { default: 200 }, height: { default: 200 } })
</script>

<template>
  <Vue3Lottie :animation-data="animationData" :width="width" :height="height" loop :auto-play="true" />
</template>
Enter fullscreen mode Exit fullscreen mode
---
import LottieVue from '../components/LottieVue.vue';
import loadingAnim from '../animations/loading.json';
---

<LottieVue client:visible :animation-data={loadingAnim} />
Enter fullscreen mode Exit fullscreen mode

Svelte Island (lottie-web)

<!-- src/components/LottieSvelte.svelte -->
<script>
  import { onMount, onDestroy } from 'svelte';
  export let src = '';
  export let width = 200;
  export let height = 200;

  let container;
  let anim;

  onMount(async () => {
    const lottie = (await import('lottie-web')).default;
    anim = lottie.loadAnimation({
      container,
      renderer: 'svg',
      loop: true,
      autoplay: true,
      path: src,
    });
  });

  onDestroy(() => anim?.destroy());
</script>

<div bind:this={container} style="width:{width}px;height:{height}px;" />
Enter fullscreen mode Exit fullscreen mode
---
import LottieSvelte from '../components/LottieSvelte.svelte';
---

<LottieSvelte client:visible src="/animations/icon.json" width={48} height={48} />
Enter fullscreen mode Exit fullscreen mode

Lazy Loading with client:visible

client:visible is the ideal directive for Lottie: it delays loading until the animation is actually scrolled into the viewport. This eliminates wasted resources for below-fold animations.

---
import HeroAnimation from '../components/HeroAnimation.jsx';
import FeatureAnimation from '../components/FeatureAnimation.jsx';
import FooterAnimation from '../components/FooterAnimation.jsx';
---

<!-- Above fold: load immediately -->
<HeroAnimation client:load />

<!-- Below fold: load when visible -->
<FeatureAnimation client:visible />
<FooterAnimation client:visible />
Enter fullscreen mode Exit fullscreen mode

For a page with 5+ animations, client:visible can reduce initial JS by 60-80%.


Preloading Above-Fold Animations

Add <link rel="preload"> in the Astro <head> for hero animations that load immediately:

---
---
<html>
  <head>
    <link rel="preload" href="/animations/hero.lottie" as="fetch" crossorigin="anonymous" />
  </head>
  <body>
    <!-- hero animation loads,7without delay -->
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

Fetching Animation Data at Runtime

For large animations, avoid bundling JSON. Instead, fetch from public/ at runtime:

// src/components/LazyLottie.jsx
import { useState, useEffect } from 'react';
import Lottie from 'lottie-react';

export default function LazyLottie({ src, width = 200, height = 200 }) {
  const [animData, setAnimData] = useState(null);

  useEffect(() => {
    fetch(src)
      .then(r => r.json())
      .then(setAnimData);
  }, [src]);

  if (!animData) {
    return <div style={{ width, height, background: '#f0f0f0', borderRadius: 8 }} />;
  }

  return (
    <div style={{ width, height }}>
      <Lottie animationData={animData} loop autoplay />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
<!-- Pass src as a string (URL), not an imported JSON object -->
<LazyLottie client:visible src="/animations/heavy.json" width={400} height={400} />
Enter fullscreen mode Exit fullscreen mode

Content Collections + Lottie (MDX)

Astro's content collections work with MDX. Add animated illustrations to blog posts:

---
title: My Blog Post
---
import LottieAnimation from '../../components/LottieAnimation.jsx';

## Introduction

<LottieAnimation client:visible src="/animations/intro-icon.lottie" width={64} height={64} />

This animation plays when you scroll to this section...
Enter fullscreen mode Exit fullscreen mode

Scroll-Triggered Animation

For a "play once on scroll" effect, use client:visible on a non-looping animation:

// src/components/ScrollReveal.jsx
import { useRef, useState } from 'react';
import Lottie from 'lottie-react';

export default function ScrollReveal({ animationData }) {
  const lottieRef = useRef(null);
  const [played, setPlayed] = useState(false);

  // This component is only mounted when visible (client:visible)
  // So autoplay fires at the right moment
  return (
    <Lottie
      lottieRef={lottieRef}
      animationData={animationData}
      loop={false}
      autoplay={!played}
      onComplete={() => setPlayed(true)}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode
---
import ScrollReveal from '../components/ScrollReveal.jsx';
import featureAnim from '../animations/feature.json';
---

<!-- Island activates when this enters viewport -->
<ScrollReveal client:visible animationData={featureAnim} />
Enter fullscreen mode Exit fullscreen mode

Because client:visible only mounts the island when the element enters the viewport, autoplay={true} fires exactly when the user sees it.


Performance Checklist for Astro

Check Solution
Large .json files Convert to .lottie at IconKing
Hero animation delayed Use client:load + <link rel="preload">
Below-fold animations loading early Use client:visible
Animation bundled in JS Pass src as URL string, fetch at runtime
Decorative animation wastes CPU aria-hidden="true" + client:idle

Summary

  1. Use client:visible for almost all Lottie animations — it loads only when needed
  2. Use client:load only for above-fold hero animations
  3. Place files in public/animations/ and reference by URL (not by import)
  4. Convert to .lottie at IconKing — 75% smaller = better Lighthouse scores
  5. Add <link rel="preload"> for hero animations to eliminate load flash
  6. Use client:idle for purely decorative background animations

Top comments (0)