DEV Community

Fazal Shah
Fazal Shah

Posted on

Lottie Animations with Tailwind CSS: The Complete Integration Guide

Tailwind CSS and Lottie animations are a natural pair — Tailwind handles static layout and styling, Lottie handles complex motion. This guide covers every integration pattern: sizing, responsive layouts, dark mode, hover triggers, and loading states using both utility classes and Tailwind's JIT variants.


Before You Start: Preview in IconKing

Before dropping any Lottie file into your Tailwind project, open it in IconKing:

  • See exact colors, bounds, and timing
  • Edit colors to match your Tailwind design system (match to your --color-* tokens)
  • Convert .json → .lottie for 75% smaller file size
  • Check for rendering issues before debugging in your app

Free, no login.


Setup

Install lottie-react (for React/Next.js + Tailwind) or use lottie-web directly:

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

Basic Usage: Sizing with Tailwind

Lottie animations are SVG or Canvas ℔ they respect width/height on their container. With Tailwind, control size via the wrapper div:

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

export default function LoadingSpinner() {
  return (
    <div className="w-16 h-16">
      <Lottie animationData={loadingAnim} loop />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Always set size on the wrapper div with Tailwind classes, not inline styles on the Lottie component ℔ this keeps your sizing in Tailwind's design system.


Responsive Lottie with Tailwind Breakpoints

export default function HeroAnimation() {
  return (
    {/* Small on mobile, larger on desktop */}
    <div className="w-32 h-32 md:w-64 md:h-64 lg:w-96 lg:h-96 mx-auto">
      <Lottie animationData={heroAnim} loop />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Centering and Layout Patterns

{/* Centered in a flex container */}
<div className="flex items-center justify-center min-h-screen bg-gray-50">
  <div className="w-48 h-48">
    <Lottie animationData={loadingAnim} loop />
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode
{/* Inline with text (icon pattern) */}
<button className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg">
  <div className="w-5 h-5">
    <Lottie animationData={arrowAnim} loop={false} />
  </div>
  Submit
</button>
Enter fullscreen mode Exit fullscreen mode
{/* Full-bleed hero background */}
<div className="relative w-full h-screen overflow-hidden">
  <div className="absolute inset-0 flex items-center justify-center opacity-20 pointer-events-none">
    <div className="w-full max-w-4xl">
      <Lottie animationData={bgAnim} loop />
    </div>
  </div>
  <div className="relative z-10 flex flex-col items-center justify-center h-full">
    <h1 className="text-5xl font-bold text-gray-900">Hero Content</h1>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Dark Mode: Reacting to Tailwind's Dark Class

Tailwind's dark mode adds a dark class to <html>. Use it with useRef and addValueCallback to change animation colors:

'use client'

import { useRef, useEffect } from 'react';
import lottie from 'lottie-web';

export default function DarkModeIcon() {
  const containerRef = useRef(null);

  useEffect(() => {
    const isDark = document.documentElement.classList.contains('dark');
    const color = isDark ? [1, 1, 1, 1] : [0.1, 0.1, 0.1, 1];

    const anim = lottie.loadAnimation({
      container: containerRef.current,
      renderer: 'svg',
      loop: true,
      autoplay: true,
      path: '/animations/icon.json',
    });

    anim.addEventListener('DOMLoaded', () => {
      anim.addValueCallback(['**', 'Fill 1', 'Color'], () => color);
    });

    return () => anim.destroy();
  }, []);

  return <div ref={containerRef} className="w-8 h-8" />;
}
Enter fullscreen mode Exit fullscreen mode

For reactive dark mode (toggled at runtime), listen for class changes:

'use client'

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

function useDarkMode() {
  const [isDark, setIsDark] = useState(
    () => document.documentElement.classList.contains('dark')
  );

  useEffect(() => {
    const observer = new MutationObserver(() => {
      setIsDark(document.documentElement.classList.contains('dark'));
    });
    observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
    return () => observer.disconnect();
  }, []);

  return isDark;
}

export default function ThemedAnimation() {
  const containerRef = useRef(null);
  const isDark = useDarkMode();

  useEffect(() => {
    const color = isDark ? [1, 1, 1, 1] : [0.15, 0.15, 0.15, 1];
    const anim = lottie.loadAnimation({
      container: containerRef.current,
      renderer: 'svg',
      loop: true,
      autoplay: true,
      path: '/animations/icon.json',
    });
    anim.addEventListener('DOMLoaded', () => {
      anim.addValueCallback(['**', 'Fill 1', 'Color'], () => color);
    });
    return () => anim.destroy();
  }, [isDark]);

  return <div ref={containerRef} className="w-10 h-10" />;
}
Enter fullscreen mode Exit fullscreen mode

Hover Trigger with Tailwind Group

Tailwind's group / group-hover utilities let you trigger an animation when a parent element is hovered:

'use client'

import { useRef } from 'react';
import Lottie from 'lottie-react';
import hoverAnim from './animations/hover-icon.json';

export default function HoverCard() {
  const lottieRef = useRef(null);

  return (
    <div
      className="group flex items-center gap-3 p-4 rounded-xl border border-gray-200 hover:border-blue-400 hover:bg-blue-50 transition-colors cursor-pointer"
      onMouseEnter={() => { lottieRef.current?.stop(); lottieRef.current?.play(); }}
      onMouseLeave={() => lottieRef.current?.stop()}
    >
      <div className="w-10 h-10 shrink-0">
        <Lottie
          lottieRef={lottieRef}
          animationData={hoverAnim}
          autoplay={false}
          loop={false}
        />
      </div>
      <div>
        <p className="font-medium text-gray-900 group-hover:text-blue-700 transition-colors">
          Card Title
        </p>
        <p className="text-sm text-gray-500">Card description</p>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Loading States with Tailwind

The most common Lottie + Tailwind pattern is replacing a loading spinner:

'use client'

import { useState } from 'react';
import Lottie from 'lottie-react';
import loadingAnim from './animations/loading.json';
import successAnim from './animations/success.json';

type Status = 'idle' | 'loading' | 'success';

export default function SubmitButton() {
  const [status, setStatus] = useState<Status>('idle');

  async function handleClick() {
    setStatus('loading');
    await new Promise(r => setTimeout(r, 2000));
    setStatus('success');
    setTimeout(() => setStatus('idle'), 2000);
  }

  return (
    <button
      onClick={handleClick}
      disabled={status !== 'idle'}
      className="relative flex items-center justify-center w-40 h-11 rounded-lg bg-blue-600 text-white font-medium disabled:opacity-60 transition-opacity"
    >
      {status === 'idle' && 'Submit'}

      {status === 'loading' && (
        <div className="w-7 h-7">
          <Lottie animationData={loadingAnim} loop />
        </div>
      )}

      {status === 'success' && (
        <div className="w-7 h-7">
          <Lottie
            animationData={successAnim}
            loop={false}
            onComplete={() => setStatus('idle')}
          />
        </div>
      )}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Skeleton + Lottie Loading Placeholder

While the animation file fetches, show a Tailwind skeleton:

'use client'

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

export default function LazyLottie({ src, className }: { src: string; className?: string }) {
  const [animData, setAnimData] = useState(null);

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

  if (!animData) {
    return (
      <div className={`bg-gray-200 animate-pulse rounded-lg ${className}`} />
    );
  }

  return (
    <div className={className}>
      <Lottie animationData={animData} loop />
    </div>
  );
}

// Usage
// <LazyLottie src="/animations/hero.json" className="w-64 h-64" />
Enter fullscreen mode Exit fullscreen mode

Scroll-Triggered Animation with Tailwind

Use IntersectionObserver with Tailwind layout classes:

'use client'

import { useRef, useEffect } from 'react';
import Lottie, { LottieRefCurrentProps } from 'lottie-react';
import animData from './animations/feature.json';

export default function ScrollReveal() {
  const wrapperRef = useRef<HTMLDivElement>(null);
  const lottieRef = useRef<LottieRefCurrentProps>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          lottieRef.current?.stop();
          lottieRef.current?.play();
        } else {
          lottieRef.current?.pause();
        }
      },
      { threshold: 0.3 }
    );
    if (wrapperRef.current) observer.observe(wrapperRef.current);
    return () => observer.disconnect();
  }, []);

  return (
    <section className="py-24 px-6">
      <div className="max-w-5xl mx-auto grid md:grid-cols-2 gap-12 items-center">
        <div ref={wrapperRef} className="w-full aspect-square max-w-sm mx-auto">
          <Lottie
            lottieRef={lottieRef}
            animationData={animData}
            autoplay={false}
            loop={false}
          />
        </div>
        <div className="space-y-4">
          <h2 className="text-3xl font-bold text-gray-900">Feature Title</h2>
          <p className="text-lg text-gray-600">Description text here.</p>
        </div>
      </div>
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

Aspect Ratio Tip

Use aspect-square instead of matching w- and h- values to keep animations perfectly square without repeating the size class:

<div className="w-48 aspect-square">
  <Lottie animationData={anim} loop />
</div>
Enter fullscreen mode Exit fullscreen mode

This also prevents accidental stretching when the container width changes responsively.


Performance Tips

1. Use object-contain behavior

Set preserveAspectRatio if the animation looks stretched:

lottie.loadAnimation({
  container,
  renderer: 'svg',
  rendererSettings: {
    preserveAspectRatio: 'xMidYMid meet'
  },
  // ...
});
Enter fullscreen mode Exit fullscreen mode

2. Use .lottie format for better LCP

Switch to @lottiefiles/dotlottie-react and convert files at IconKing — 75% smaller = faster initial paint.

3. Lazy load large animations with next/dynamic

import dynamic from 'next/dynamic';

const HeroAnimation = dynamic(
  () => import('@/components/HeroAnimation'),
  { ssr: false, loading: () => <div className="w-96 h-96 bg-gray-100 animate-pulse rounded-xl" /> }
);
Enter fullscreen mode Exit fullscreen mode

Summary

  • Size Lottie via wrapper div Tailwind classes (w-16 h-16, w-full aspect-square)
  • Use group + onMouseEnter for hover-triggered animations
  • React to Tailwind dark mode via MutationObserver on document.documentElement.classList
  • Show animate-pulse skeleton while the animation file loads
  • Use IntersectionObserver to pause off-screen animations
  • Convert to .lottie at IconKing for smaller bundles and better Lighthouse scores

Top comments (1)

Collapse
 
alexandersstudi profile image
Alexander

Relying on a wrapper div with Tailwind sizing utilities is the safest way to keep Lottie animations from breaking out of their layouts. The real friction usually hits when trying to theme them for dark mode using utilities like text-white. Unless the original After Effects file was exported to target currentColor, the SVG paths will stubbornly hold onto their raw hex values. Mapping your CSS variables directly into the Lottie JSON before mounting is usually the most bulletproof fix.