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 (0)