Tailwind Animation Library: 30 Classes for Common Effects
30 Tailwind animation classes for fade, slide, spin, pulse, and more — with real code examples, custom keyframe tricks, and performance tips for Tailwind v4.
What Tailwind Gives You Out of the Box
Honestly, most developers sleep on Tailwind's built-in animation utilities until they're deep in a project and suddenly need a spinner at 2 AM. Tailwind ships with four core animation classes: animate-spin, animate-ping, animate-pulse, and animate-bounce. That's it. Four. And yet they cover a surprisingly wide surface area of real UI patterns — loading indicators, notification badges, skeleton loaders, scroll-down cues.
In Tailwind v4.0.2 the animation system got refactored under the CSS-first config model, meaning you define custom keyframes in plain CSS rather than tailwind.config.js. This is actually a meaningful change if you've been managing animations via the old extend.keyframes object — your muscle memory will fight you for a day or two, then it clicks.
The four defaults map to these durations and timing functions: animate-spin runs at 1s linear infinite, animate-ping at 1s cubic-bezier(0,0,0.2,1) infinite, animate-pulse at 2s cubic-bezier(0.4,0,0.6,1) infinite, and animate-bounce at 1s infinite with a custom ease curve. Knowing the defaults lets you override only what you need without repeating everything.
Building a Fade and Slide Animation Set in Tailwind v4
Fade-in and slide-in effects are the bread and butter of any UI. Tailwind doesn't ship these by default, but in v4 you can register them in a @layer block inside your global CSS file — no config file required. Here's a practical set that covers the most common entrance patterns.
@layer utilities {
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-up {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slide-down {
from { opacity: 0; transform: translateY(-16px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slide-left {
from { opacity: 0; transform: translateX(16px); }
to { opacity: 1; transform: translateX(0); }
}
.animate-fade-in { animation: fade-in 0.3s ease-out both; }
.animate-slide-up { animation: slide-up 0.4s ease-out both; }
.animate-slide-down{ animation: slide-down 0.4s ease-out both; }
.animate-slide-left{ animation: slide-left 0.35s ease-out both; }
}The both fill-mode is the detail people forget. Without it, elements snap back to their initial state before the animation fires if there's any delay. both means the element holds the from state before it starts and the to state after it ends. Use it by default and only remove it when you know you don't want that behavior.
30 Animation Classes You'll Actually Use
Let's go through the full list categorized by effect type. These are classes you can copy into your global CSS or a dedicated animations.css file imported at the root. Each one targets a specific, recurring UI pattern — not abstract art.
Entrance effects (8 classes): animate-fade-in, animate-fade-out, animate-slide-up, animate-slide-down, animate-slide-left, animate-slide-right, animate-zoom-in, animate-zoom-out. These handle modal open/close, toast notifications, dropdown menus, and drawer panels. The slide variants use a 16px offset — enough to feel intentional, not so much that it drags.
Attention effects (7 classes): animate-shake, animate-wiggle, animate-jello, animate-rubber-band, animate-flash, animate-tada, animate-swing. Use these sparingly. animate-shake is perfect for a failed form validation. animate-flash works on a save confirmation badge. animate-wiggle on an empty cart icon. More than two of these on one page and it starts feeling like a MySpace profile from 2006.
Looping effects (8 classes): animate-spin (built-in), animate-ping (built-in), animate-pulse (built-in), animate-bounce (built-in), animate-spin-slow (3s), animate-spin-fast (0.5s), animate-float, animate-heartbeat. The animate-float effect — a gentle 6px vertical oscillation at 3s ease-in-out infinite — is useful for hero section illustrations or feature cards. It reads as 'alive' without being distracting.
Skeleton and loading effects (4 classes): animate-shimmer, animate-skeleton, animate-progress, animate-scan. The shimmer is the classic gradient sweep used by Facebook, LinkedIn, and YouTube for content placeholders. It requires a gradient background on the element itself — the animation just moves the background-position. State transition effects (3 classes): animate-pop, animate-squeeze, animate-flip. These work best triggered by user interaction rather than on mount. Check out how particles-background-react uses state-driven class toggling for a similar approach.
The Shimmer Skeleton Pattern in Detail
The shimmer effect deserves its own section because it's tricky to get right and the naive version looks off. The issue is that most tutorials animate opacity for skeleton loaders, which produces a dull pulse. The real shimmer moves a gradient across the element, which reads as light reflecting off a surface — it's more convincing.
// globals.css
@layer utilities {
@keyframes shimmer {
0% { background-position: -400px 0; }
100% { background-position: 400px 0; }
}
.animate-shimmer {
background: linear-gradient(
90deg,
rgba(255,255,255,0.05) 25%,
rgba(255,255,255,0.15) 50%,
rgba(255,255,255,0.05) 75%
);
background-size: 800px 100%;
animation: shimmer 1.4s ease-in-out infinite;
}
}
// SkeletonCard.tsx
export function SkeletonCard() {
return (
<div className="rounded-xl bg-white/5 p-4 space-y-3">
<div className="h-4 w-2/3 rounded animate-shimmer" />
<div className="h-3 w-full rounded animate-shimmer" />
<div className="h-3 w-4/5 rounded animate-shimmer" />
</div>
);
}Notice the background-size: 800px 100%. That 800px value needs to be roughly double the width of your largest skeleton element so the gradient sweep looks continuous. Too small and it stutters at the edges. Too large and it moves too slowly to read as a shimmer. For most card-width content, 800px is the sweet spot. If you're building a full-page skeleton, go to 1200px.
Controlling Duration and Delay with Arbitrary Values
Tailwind's arbitrary value syntax makes one-off animation tuning fast. You don't need a custom class for every duration variant — just use [duration] inline. duration-[350ms] on an element with animate-slide-up gives you that specific timing without touching your CSS. Same for delays: delay-[120ms] staggers a list of items without writing a loop.
Staggered list animations are where this shines. Instead of generating 10 CSS classes manually, do it in JSX with an inline style for the delay. This keeps the keyframe definition in CSS while the dynamic part stays in the component where it belongs — a clean separation that's easy to maintain.
const items = ['Dashboard', 'Analytics', 'Settings', 'Billing'];
export function NavList() {
return (
<ul className="space-y-2">
{items.map((item, i) => (
<li
key={item}
className="animate-slide-left opacity-0"
style={{ animationDelay: `${i * 80}ms`, animationFillMode: 'forwards' }}
>
{item}
</li>
))}
</ul>
);
}One gotcha: when you set opacity-0 as a base class and use animationFillMode: 'forwards', the element starts invisible and stays in the final to state after the animation completes. That's exactly what you want. Without forwards, elements would flash invisible again after the animation ends. If you're also using tailwind-v4-features like CSS variable tokens, you can drive the delay via a custom property on the element for an even cleaner pattern.
Performance: Which Animations Are Safe and Which Aren't
Not all animations are equal from a performance standpoint. The browser can only composite transform and opacity animations on the GPU without triggering layout or paint. Everything else — width, height, top, left, background-color, box-shadow — causes the browser to recalculate layout or repaint pixels on the CPU, which means dropped frames on lower-end devices.
Stick to transform and opacity for all your animation keyframes. If you need a grow effect, use scale() inside transform, not width. If you need a color shift, consider using opacity on a pseudo-element with a different background rather than animating background-color directly. This sounds like overkill until you test on a mid-range Android device and see what 'animating background-color' actually looks like at 30fps.
Also, add will-change: transform to elements you know will animate — but only right before the animation starts, not permanently. A common pattern is to apply it on hover or on component mount, then remove it on animation end. Leaving will-change on permanently wastes GPU memory on elements that aren't moving. For glassmorphism cards with multiple animated layers, this matters — see tailwind-glassmorphism-advanced for a worked example.
The prefers-reduced-motion media query is non-negotiable. Wrap all your looping and entrance animations in a check. In Tailwind v4 you can use the motion-safe: and motion-reduce: variants directly on classes, which is the cleanest approach. motion-reduce:animate-none on any animated element is a one-liner that respects accessibility requirements.
Integrating Animation Classes with React State and Framer Motion
Pure CSS animations are great for entrance effects and looping states, but they don't handle exit animations. When you remove an element from the DOM, it just disappears — there's no 'out' phase. This is where you either reach for a small state machine or bring in Framer Motion for the exit-animation use case specifically.
The lightweight state machine approach: keep a boolean isVisible in state, toggle it on close, apply an animate-fade-out class when !isVisible, and remove the element from the DOM after the animation duration elapses. A useEffect with setTimeout tied to the CSS duration does the job cleanly for modals, toasts, and drawers. It's not fancy but it works without any additional dependencies.
When you do need Framer Motion, don't throw out your Tailwind animation utilities — use them for initial page animations and hover effects, and let Framer handle only exit sequences. The combination keeps your bundle from growing unnecessarily and keeps simple effects simple. If you're building a theme-toggle-react component, for example, the icon swap animation is a perfect candidate for pure Tailwind classes rather than Framer Motion.
Organizing Your Animation Utilities for a Real Project
Where do these classes actually live in a production codebase? Don't scatter keyframes across component CSS files. Create a single animations.css file in your styles directory and import it once in your root layout. Every custom animation class lives there. Developers know exactly where to look when something moves unexpectedly.
Name your classes predictably. The pattern animate-{effect}-{variant} — like animate-fade-in, animate-slide-up-fast, animate-shimmer-dark — makes autocomplete useful and makes grep results readable. Avoid names like animate-thing or animate-custom1. Future you will be grateful.
Document the intended use case as a comment above each keyframe block. Not a novel — one line. /* Badge pulse for unread notifications */ above @keyframes notification-ping saves five minutes of archaeology every time someone touches it. Animation code has a high WTF-per-minute ratio when it's undocumented because the visual output isn't obvious from the CSS values. Pair this with consistent tailwind-component-patterns across your project and your team will move significantly faster.
Finally, test your animation classes in isolation before composing them. Drop a <div className="animate-slide-up bg-red-500 w-16 h-16" /> in a scratch page and verify the timing, fill mode, and motion direction are exactly what you expect before wiring up the real component. It's a 30-second check that saves hours of debugging compound effects.
FAQ
In Tailwind v4.0.2, define @keyframes and utility classes inside @layer utilities in your global CSS file. The tailwind.config.js extend.keyframes approach still works if you're using the compatibility layer, but the CSS-first method is the v4 way — no config file needed.
You need animation-fill-mode: both (or the shorthand both at the end of your animation value). Without it, the element renders at its natural opacity before the keyframe fires. Set both and the element will hold the from state until the animation starts, then hold the to state after it ends.
Yes. Animation classes are just CSS — they don't require client-side JavaScript. You can apply animate-spin, animate-pulse, or any custom utility class to server components without adding 'use client'. The only exception is when you're toggling classes dynamically based on React state, which requires a client component.
animate-ping scales the element up and fades it out — it's designed for notification badges and 'live' indicators, like a radar ping. animate-pulse cycles opacity between 50% and 100% at a slower rate — it's designed for skeleton loaders to suggest content is incoming. They look similar in screenshots but feel very different in motion.
Use the motion-reduce: and motion-safe: variants. For example: motion-reduce:animate-none disables an animation entirely for users who prefer reduced motion. You can also write motion-safe:animate-bounce to only apply the class when the user hasn't opted out of motion. These variants work on any animation or transition class.
CSS animations are the right default for entrance effects, looping indicators, and hover responses. They're zero-JS and perform well since you're animating transform and opacity on the compositor thread. Reach for Framer Motion specifically when you need exit animations, layout animations, or gesture-driven physics — not for everything.