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â.lottiefor 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
For Vue islands:
npx astro add vue
npm install vue3-lottie
For dotLottie (recommended for .lottie format):
npx astro add react
npm install @lottiefiles/dotlottie-react
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>
);
}
---
// 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>
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 }}
/>
);
}
---
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}
/>
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>
---
import LottieVue from '../components/LottieVue.vue';
import loadingAnim from '../animations/loading.json';
---
<LottieVue client:visible :animation-data={loadingAnim} />
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;" />
---
import LottieSvelte from '../components/LottieSvelte.svelte';
---
<LottieSvelte client:visible src="/animations/icon.json" width={48} height={48} />
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 />
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>
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>
);
}
<!-- Pass src as a string (URL), not an imported JSON object -->
<LazyLottie client:visible src="/animations/heavy.json" width={400} height={400} />
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...
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)}
/>
);
}
---
import ScrollReveal from '../components/ScrollReveal.jsx';
import featureAnim from '../animations/feature.json';
---
<!-- Island activates when this enters viewport -->
<ScrollReveal client:visible animationData={featureAnim} />
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
- Use
client:visiblefor almost all Lottie animations â it loads only when needed - Use
client:loadonly for above-fold hero animations - Place files in
public/animations/and reference by URL (not byimport) - Convert to
.lottieat IconKing â 75% smaller = better Lighthouse scores - Add
<link rel="preload">for hero animations to eliminate load flash - Use
client:idlefor purely decorative background animations
Top comments (0)