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