tailwindcss-animate Plugin: Fade, Slide, Scale Entry Animations
Learn how to use tailwindcss-animate to add fade, slide, and scale entry animations to any Tailwind project — with real code, gotchas, and composable patterns.
What tailwindcss-animate Actually Does
If you've ever wanted smooth enter/exit animations without dropping a 40 kB motion library into your bundle, tailwindcss-animate is probably already what you're looking for. It was created by shadcn (yes, that shadcn) and ships bundled inside every shadcn/ui project since v0.3.0. But it works totally standalone — just a Tailwind plugin, no runtime JS.
The plugin adds a set of CSS custom properties and a handful of keyframe animations to your stylesheet: animate-in, animate-out, fade-in, fade-out, slide-in-from-top, slide-out-to-bottom, zoom-in, zoom-out, and composable modifiers like duration-300 or ease-in-out. You stack them with Tailwind's regular utility syntax, which means you get full Tailwind autocomplete in VS Code for free.
Honestly, the design is clever. Every animation is built around CSS variables (--tw-enter-opacity, --tw-enter-translate-x, etc.) that the modifier utilities set. The animate-in class applies a single keyframe that reads those variables. You just layer modifiers on top. It's like a mini animation DSL that plays nicely with JIT — no purge issues, no surprises.
Worth noting: this isn't the same as Tailwind's built-in animate-{name} classes like animate-spin or animate-bounce. Those are one-off keyframe presets. tailwindcss-animate is a composable system where every entry animation shares the same underlying keyframe.
Installation and Setup
Setup takes about 90 seconds. Install the package, register it as a plugin, done.
npm install tailwindcss-animate
# or
pnpm add tailwindcss-animateThen open your tailwind.config.ts (or .js) and add it to the plugins array:
``ts
// tailwind.config.ts
import type { Config } from 'tailwindcss';
import animate from 'tailwindcss-animate';
const config: Config = {
content: ['./src/**/*.{ts,tsx}'],
plugins: [animate],
};
export default config;
``
That's it. No @layer declarations, no custom keyframe blocks, no CSS imports. The plugin injects everything at build time. If you're on Tailwind v4 (released in early 2025), the plugin still works — you'd register it in your CSS file with @plugin 'tailwindcss-animate' instead.
Quick aside: if you're already using shadcn/ui, this plugin is already in your project. You can open your tailwind.config.ts and look for it — it's almost always the last item in the plugins array.
Fade Animations
Fade is the simplest animation the plugin offers, and it's the one you'll reach for most often — modals, tooltips, dropdown menus, any element that pops in and out of the DOM.
// A card that fades in on mount
<div className="animate-in fade-in duration-300">
<p>I appear smoothly.</p>
</div>The fade-in modifier sets --tw-enter-opacity: 0, which means the element starts completely transparent and transitions to full opacity over whatever duration-* you pick. The default if you omit duration is 150ms. In practice, duration-200 to duration-300 feels natural for most UI elements — anything shorter reads as a glitch, anything past 500ms starts feeling sluggish.
For exit animations you swap animate-in for animate-out and fade-in for fade-out:
``tsx
// Combine with conditional class logic for enter/exit
<div
className={
${isVisible
? 'animate-in fade-in duration-200'
: 'animate-out fade-out duration-150'
}
}
>
Content
</div>
``
One thing to keep in mind: exit animations require the element to still be in the DOM while the animation plays. If you're using React state and immediately removing the element, the animation never runs. Pair it with a useEffect delay or use a library like framer-motion's AnimatePresence for controlled unmounting. Alternatively, Radix UI's Dialog and Popover primitives already handle this — which is why tailwindcss-animate and shadcn/ui pair so naturally.
Slide Animations
Slide animations are directional — elements enter from or exit toward a specific edge. The plugin ships four directions: top, bottom, left, right. Each one is available as both an enter and exit variant.
// Slide in from the bottom — common for mobile sheet drawers
<div className="animate-in slide-in-from-bottom duration-300 ease-out">
Drawer content
</div>
// Slide in from the left — sidebar pattern
<div className="animate-in slide-in-from-left-72 duration-200">
Sidebar
</div>Wait, slide-in-from-left-72? Yes — the directional utilities accept a distance suffix that maps to Tailwind's spacing scale. slide-in-from-top-4 translates the element 16px vertically before animating in. You can use any value on the default spacing scale (0, 1, 2, 4, 8, 16, full, etc.) or an arbitrary value via square brackets: slide-in-from-top-[120px].
In practice, a small offset like slide-in-from-top-4 (16px) feels more polished than a full-screen-height entry. The element appears to "settle" into place rather than flying across the viewport. For notification toasts entering from the top-right, slide-in-from-top-2 with a fade-in stacked on top is the combination I'd recommend every time.
// Stacking fade + slide is the real power move
<div className="animate-in fade-in slide-in-from-top-2 duration-300 ease-out">
Toast notification
</div>The stacking works because each modifier just sets a different CSS variable. They don't conflict — they compose. That same keyframe reads all variables simultaneously and applies the combined transform and opacity.
Scale (Zoom) Animations
Scale animations add a subtle size change on top of opacity. The zoom-in modifier starts the element smaller than its final size; zoom-out shrinks it on exit. Both accept a numeric suffix that maps to a percentage: zoom-in-50 starts at 50% scale, zoom-in-95 starts at 95% scale.
// Popover-style scale enter — very satisfying at zoom-in-95
<div className="animate-in fade-in zoom-in-95 duration-150 ease-out">
Popover content
</div>
// Dialog / modal that scales up from the center
<dialog className="animate-in fade-in zoom-in-90 duration-200 ease-out">
Modal body
</dialog>Look, zoom-in-95 is the sweet spot for most UI popups. It's subtle enough that you barely notice it consciously, but take it away and the popup suddenly feels flat and harsh. The difference between zoom-in-95 and zoom-in-50 is the difference between a polished product and a loading screen from 2009.
You can combine scale with slide for more theatrical entries — think a hero section or a feature card that slides in from below *and* scales up:
``tsx
<section className="animate-in fade-in slide-in-from-bottom-8 zoom-in-95 duration-500 ease-out">
Hero content
</section>
``
For UI components that need to feel snappy rather than dramatic — dropdowns, context menus, tooltips — keep scale ranges tight (zoom-in-95 to zoom-in-[0.98]) and durations short (100ms–150ms). Reserve zoom-in-50 or lower for onboarding moments where you want the user to notice the animation.
Composing Animations with Tailwind and Empire UI Components
The real value of this plugin is how naturally it composes with the rest of your Tailwind stack. You're not writing custom @keyframes, you're not importing a motion component — you're just stacking utility classes the same way you already style color and spacing. That consistency is worth something.
If you're building components that need this kind of motion polish, Empire UI ships components that already wire up enter/exit animations correctly. Rather than building a <Modal> from scratch and discovering on day two that your exit animation never fires, you can start from a solid base and customize from there.
For glassmorphism-style modals and panels — where the frosted surface needs to feel premium — stacking backdrop-blur-md with animate-in fade-in zoom-in-95 gives you a result that actually looks intentional. Check out the glassmorphism components section to see this in context.
// A glassmorphism panel with entry animation
<div
className={[
// glass surface
'bg-white/10 backdrop-blur-md border border-white/20 rounded-2xl p-6',
// entry animation
'animate-in fade-in zoom-in-95 slide-in-from-bottom-4 duration-300 ease-out',
].join(' ')}
>
<h2 className="text-white font-semibold text-xl">Glass Panel</h2>
<p className="text-white/70 mt-2">Smooth entry, frosted surface.</p>
</div>One more thing — if you're building a component library yourself and want to expose animation variants as props, a clean pattern is to store the class strings in a const map and spread them in:
``tsx
const enterVariants = {
fade: 'animate-in fade-in duration-200',
slide: 'animate-in fade-in slide-in-from-bottom-4 duration-300',
scale: 'animate-in fade-in zoom-in-95 duration-150',
} as const;
type EnterVariant = keyof typeof enterVariants;
function Card({ enter = 'fade' }: { enter?: EnterVariant }) {
return <div className={enterVariants[enter]}>Content</div>;
}
``
Performance, Accessibility, and When Not to Use This
CSS keyframe animations run on the compositor thread in modern browsers — Chromium, Firefox, and Safari all handle transform and opacity animations without touching the main thread as of 2024. That means tailwindcss-animate is genuinely low-cost. You're not paying JS execution time for these transitions.
Accessibility is where you need to pay attention. Always wrap your animation classes in a motion-safe: variant to respect the user's OS-level reduced motion preference:
``tsx
<div className="motion-safe:animate-in motion-safe:fade-in motion-safe:duration-300">
Animated only when motion is safe
</div>
`
Tailwind's motion-safe: and motion-reduce:` variants work perfectly with this plugin. There's no excuse for skipping them — the syntax is one extra prefix.
That said, not everything needs an animation. Navigation links, form inputs, static text — animating everything makes the UI feel chaotic and slows down power users who are just trying to get work done. Reserve enter animations for: modals, drawers, toasts, dropdown menus, and page-level transitions. Everything else? Let it appear instantly.
Worth noting: if your animation is driven by Radix UI's data-state attribute (like data-state="open" on a dialog), you can target it with Tailwind's data-* variants instead of managing state manually:
``tsx
<div
data-state={isOpen ? 'open' : 'closed'}
className="
data-[state=open]:animate-in data-[state=open]:fade-in data-[state=open]:zoom-in-95
data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=closed]:zoom-out-95
duration-200
"
>
Radix-compatible panel
</div>
`
This is exactly the pattern shadcn/ui` uses internally, and it's worth stealing.
FAQ
Yes, but the registration syntax changes. Instead of adding it to plugins in tailwind.config.ts, you add @plugin 'tailwindcss-animate' directly in your CSS file. Everything else — class names, composability — stays the same.
The exit animation needs time to play before the DOM node disappears. Use a useEffect with a setTimeout matching your animation duration, or use Radix UI's built-in forceMount + data-state approach. Framer Motion's AnimatePresence is another option if you're already in that ecosystem.
Absolutely. It's a standalone Tailwind plugin with no dependency on shadcn. Just install it, add it to your plugins array, and start using the classes. shadcn bundles it because it's great, not because there's a tight coupling.
Tailwind transitions (transition-opacity duration-300) work on CSS property changes — useful for hover states. animate-in fade-in triggers a CSS keyframe, which fires once on mount. Use transitions for interactive state changes, keyframes for element entry/exit.