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â.lottiefor 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+onMouseEnterfor hover-triggered animations - React to Tailwind dark mode via
MutationObserverondocument.documentElement.classList - Show
animate-pulseskeleton while the animation file loads - Use
IntersectionObserverto pause off-screen animations - Convert to
.lottieat IconKing for smaller bundles and better Lighthouse scores
Top comments (1)
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 targetcurrentColor, 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.