VOOZH about

URL: https://dev.to/fazalshah/lottie-animations-with-tailwind-css-the-complete-integration-guide-1hl1

⇱ Lottie Animations with Tailwind CSS: The Complete Integration Guide - DEV Community


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

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>
 );
}

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>
 );
}

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>
{/* 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>
{/* 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>

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" />;
}

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" />;
}

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>
 );
}

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>
 );
}

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" />

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>
 );
}

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>

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'
 },
 // ...
});

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" /> }
);

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