DEV Community

Fazal Shah
Fazal Shah

Posted on

Lottie Animations in Gatsby.js: A Complete Guide

Gatsby's static site generation requires specific patterns for Lottie — animations need to be excluded from SSR, JSON imports need proper webpack config, and performance needs special attention for static pages. This guide covers all of it.


The Core SSR Problem

Gatsby generates HTML at build time. Lottie animations reference window, document, and navigator — cone of which exist during server-side rendering. If you import Lottie without precautions, builds fail with:

WebpackError: ReferenceError: window is not defined
Enter fullscreen mode Exit fullscreen mode

The fix: use Gatsby's dynamic import with { ssr: false }, or wrap initialization in typeof window !== 'undefined' checks.


Before You Build

Open your animation files in IconKing first:

  • Preview exactly how the animation renders
  • Edit colors to match your Gatsby site's design system
  • Convert .json → .lottie for 75% smaller files
  • Catch unsupported effects before they fail silently in production

Free, no login.


Installation

npm install lottie-react
# or for .lottie format
npm install @lottiefiles/dotlottie-react
Enter fullscreen mode Exit fullscreen mode

Option 1: gatsby-plugin-no-ssr (Simplest)

The simplest approach: wrap your animated component in the NoSSR pattern. Gatsby provides this natively using loadable-components:

npm install @loadable/component
Enter fullscreen mode Exit fullscreen mode
// src/components/AnimatedHero.jsx
import loadable from '@loadable/component';

const LottieAnimation = loadable(
  () => import('./LottiePlayer'),
  { ssr: false }
);

export function AnimatedHero() {
  return (
    <section>
      <h1>Hero Title</h1>
      <LottieAnimation />
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode
// src/components/LottiePlayer.jsx
import Lottie from 'lottie-react';
import heroAnim from '../animations/hero.json';

export default function LottiePlayer() {
  return (
    <div style={{ width: 400, height: 400 }}>
      <Lottie animationData={heroAnim} loop autoplay />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

This completely excludes the Lottie component from SSR and hydrates it on the client.


Option 2: useEffect Guard Pattern

If you prefer not to use an extra library, guard the animation initialization:

import { useState, useEffect } from 'react';

export function SafeLottie({ animationData, ...props }) {
  const [isBrowser, setIsBrowser] = useState(false);

  useEffect(() => {
    setIsBrowser(true);
  }, []);

  if (!isBrowser) return null;

  // Dynamically import Lottie only on the client
  const Lottie = require('lottie-react').default;

  return (
    <div style={{ width: 200, height: 200 }}>
      <Lottie animationData={animationData} {...props} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Option 3: Gatsbys gatsby-browser.js Approach

For animations that appear on every page (e.g., nav icons), initialize globally in gatsby-browser.js:

// gatsby-browser.js
export const onClientEntry = () => {
  // Lottie initializations that need window
  window.__lottieReady = true;
};
Enter fullscreen mode Exit fullscreen mode

Then check window.__lottieReady in your components.


Working with JSON Files

Gatsby handles .json imports via webpack. The standard require pattern works:

import Lottie from 'lottie-react';
import animData from '../animations/loading.json';

// animData is automatically available as a JavaScript object
<Lottie animationData={animData} loop autoplay />
Enter fullscreen mode Exit fullscreen mode

For larger animation files, place them in static/animations/ and load at runtime to keep them out of the JS bundle:

import { useState, useEffect } from 'react';
import Lottie from 'lottie-react';

export function LazyAnimation({ src }) {
  const [animData, setAnimData] = useState(null);

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

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

  return (
    <div style={{ width: 200, height: 200 }}>
      <Lottie animationData={animData} loop autoplay />
    </div>
  );
}

// Usage ↔ file in static/animations/hero.json
<LazyAnimation src="/animations/hero.json" />
Enter fullscreen mode Exit fullscreen mode

Using dotLottie in Gatsby

For .lottie format (75% smaller), use @lottiefiles/dotlottie-react:

npm install @lottiefiles/dotlottie-react
Enter fullscreen mode Exit fullscreen mode
// Place files in static/animations/
// Reference as absolute path from /

import loadable from '@loadable/component';

const DotLottieAnim = loadable(
  async () => {
    const { DotLottieReact } = await import('@lottiefiles/dotlottie-react');
    return function DotLottieWrapper(props) {
      return <DotLottieReact {...props} />;
    };
  },
  { ssr: false }
);

export function HeroAnimation() {
  return (
    <DotLottieAnim
      src="/animations/hero.lottie"
      loop
      autoplay
      style={{ width: 400, height: 400 }}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

gatsby-config.js: No Changes Needed

Unlike some libraries, Lottie doesn't require any Gatsby plugin configuration. The @loadable/component approach handles everything at the component level.


IntersectionObserver for Gatsby (Scroll Animations)

Gatsby sites are often content-heavy. Pause off-screen animations to prevent performance issues on long pages:

import { useRef, useEffect, useState } from 'react';
import loadable from '@loadable/component';

const Lottie = loadable(() => import('lottie-react'), {
  ssr: false,
  resolveComponent: mod => mod.default,
});

export function ScrollRevealAnimation({ animationData }) {
  const wrapperRef = useRef(null);
  const lottieRef = useRef(null);
  const [shouldRender, setShouldRender] = useState(false);

  useEffect(() => {
    if (typeof IntersectionObserver === 'undefined') return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setShouldRender(true); // render once visible
          lottieRef.current?.play();
        } else {
          lottieRef.current?.pause();
        }
      },
      { threshold: 0.2 }
    );

    if (wrapperRef.current) observer.observe(wrapperRef.current);
    return () => observer.disconnect();
  }, []);

  return (
    <div style={{ width: 300, height: 300 }} ref={wrapperRef}>
      {shouldRender && (
        <Lottie
          lottieRef={lottieRef}
          animationData={animationData}
          autoplay={false}
          loop={false}
        />
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Gatsby Image + Lottie: Hero Section Pattern

import { StaticImage } from 'gatsby-plugin-image';
import loadable from '@loadable/component';

const LottieAnim = loadable(() => import('../components/LottiePlayer'), { ssr: false });

export default function HeroSection() {
  return (
    <section className="hero">
      <div className="hero-content">
        <h1>Your Product</h1>
        <p>Tagline here</p>
      </div>
      <div className="hero-animation">
        <LottieAnim />
      </div>
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

MDX / Blog Post Animations

Gatsby is popular for blogs (via MDX). Add animated illustrations to MDX posts:

// src/components/mdx/AnimatedCallout.jsx
import { useState, useEffect } from 'react';

export function AnimatedCallout({ src, children }) {
  const [Lottie, setLottie] = useState(null);
  const [animData, setAnimData] = useState(null);

  useEffect(() => {
    import('lottie-react').then(mod => setLottie(() => mod.default));
    fetch(src).then(r => r.json()).then(setAnimData);
  }, [src]);

  return (
    <div style={{ display: 'flex', gap: 16, padding: 16, background: '#f9f9f9', borderRadius: 8 }}>
      {Lottie && animData && (
        <div style={{ width: 48, height: 48, flexShrink: 0 }}>
          <Lottie animationData={animData} loop autoplay />
        </div>
      )}
      <div>{children}</div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In your gatsby-config.js, add it to mdxOptions.components:

// gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: 'gatsby-plugin-mdx',
      options: {
        mdxOptions: {
          remarkPlugins: [],
        },
      },
    },
  ],
};
Enter fullscreen mode Exit fullscreen mode
import { AnimatedCallout } from '../components/mdx/AnimatedCallout'

<AnimatedCallout src="/animations/tip.json">
  This is a callout with an animated icon!
</AnimatedCallout>
Enter fullscreen mode Exit fullscreen mode

Performance Checklist for Gatsby

  • Use .lottie format (convert at IconKing) ↔ smaller files = better Lighthouse scores
  • Keep large animations in static/animations/ (not bundled in JS)
  • Use @loadable/component with { ssr: false } for all Lottie components
  • Add <link rel="preload"> in gatsby-ssr.js for above-fold animations:
// gatsby-ssr.js
import React from 'react';

export const onRenderBody = ({ setHeadComponents }) => {
  setHeadComponents([
    <link
      key="preload-hero-lottie"
      rel="preload"
      href="/animations/hero.lottie"
      as="fetch"
      crossOrigin="anonymous"
    />,
  ]);
};
Enter fullscreen mode Exit fullscreen mode

Summary

  1. Always use @loadable/component with { ssr: false } — Lottie requires window
  2. For small files: bundle via import animData from './anim.json'
  3. For large files: store in static/animations/ and fetch at runtime
  4. Use IntersectionObserver to pause off-screen animations on long pages
  5. Convert to .lottie at IconKing for better Lighthouse scores
  6. Preload hero animations in gatsby-ssr.js to eliminate the flash on load

Top comments (0)