EmpireUI
Get Pro
← Blog8 min read#tailwind#animation#plugin

tailwindcss-motion Plugin: Declarative Animations in Tailwind

tailwindcss-motion brings declarative, utility-first animations to Tailwind — no custom keyframes, no JavaScript. Here's how to actually use it.

developer writing declarative CSS animation code in dark editor

What tailwindcss-motion Actually Is

If you've been wiring up custom @keyframes in your Tailwind config just to make a button bounce or a modal slide in, you already know the pain. tailwindcss-motion — released as a stable v1 in late 2024 and refined through 2025 — solves that by giving you composable animation utility classes you apply directly in markup. No config additions, no JavaScript, no separate animation library.

The mental model is straightforward. You combine a motion preset class like motion-preset-slide-up with optional modifier classes like motion-duration-300 and motion-delay-150. That's it. The plugin generates the keyframes, handles animation-fill-mode, and respects prefers-reduced-motion automatically. Worth noting: it's not a runtime library — it's a PostCSS plugin just like Tailwind itself, so your bundle stays lean.

Honestly, what surprised me most when I first used it was how well the class names read in JSX. className="motion-preset-fade motion-duration-500" is instantly obvious to any developer on your team. Compare that to the alternative: a custom CSS file, a useEffect that adds a class on mount, and a transition in your Tailwind extend block. You'd be doing all of that just to fade something in.

Quick aside: this isn't the same as Framer Motion or GSAP. Those libraries exist for complex, timeline-based, interactive animations. tailwindcss-motion targets the 80% case — entry animations, hover states, loading indicators — the stuff every UI needs and nobody wants to write by hand.

Installing and Configuring the Plugin

Setup is fast. Install the package, register it as a Tailwind plugin, and you're done. No wrapping your app in a provider, no runtime overhead.

npm install tailwindcss-motion
# or
pnpm add tailwindcss-motion
// tailwind.config.ts
import type { Config } from 'tailwindcss';
import motion from 'tailwindcss-motion';

const config: Config = {
  content: ['./src/**/*.{ts,tsx}'],
  plugins: [motion],
};

export default config;

That's the entire setup for Tailwind v3. If you're on Tailwind v4 with CSS-first config, you drop an @plugin directive in your CSS file instead — same one-liner, just a different location. The plugin surface area doesn't change between v3 and v4, which is a nice touch.

One more thing — the plugin tree-shakes perfectly because Tailwind only includes classes you actually use. A project that uses three motion presets generates CSS for exactly those three presets. You're not shipping a 40 KB animation library.

The Core Class System: Presets, Duration, Delay, Easing

The API has four main axes. Presets are named animation patterns. Duration controls how long. Delay controls when it starts. Easing controls the timing curve. Mix one from each category and you get a complete animation.

// Slide up on mount, 400ms, 100ms delay, bouncy easing
<div className="motion-preset-slide-up motion-duration-[400ms] motion-delay-100 motion-ease-spring-bouncy">
  Hello world
</div>

// Fade in with a subtle scale
<button className="motion-preset-focus motion-duration-200">
  Click me
</button>

// Staggered list items using CSS custom properties
{items.map((item, i) => (
  <li
    key={item.id}
    className="motion-preset-slide-right motion-duration-300"
    style={{ '--motion-delay': `${i * 75}ms` } as React.CSSProperties}
  >
    {item.label}
  </li>
))}

The preset list covers what you'd actually reach for day-to-day: motion-preset-fade, motion-preset-slide-up/down/left/right, motion-preset-scale-up, motion-preset-blur-in, motion-preset-expand, motion-preset-bounce, motion-preset-focus, and more. Each preset maps to a set of keyframes under the hood, but you never touch them.

Duration accepts both the predefined scale (motion-duration-75, motion-duration-150, motion-duration-300, motion-duration-500, motion-duration-700, motion-duration-1000) and arbitrary values via square-bracket syntax (motion-duration-[1200ms]). Same story for delay. In practice, I reach for motion-duration-300 and motion-duration-500 about 90% of the time — those feel natural for UI transitions without being sluggish.

The easing classes are where it gets interesting. motion-ease-spring-smooth, motion-ease-spring-bouncy, motion-ease-spring-bouncier, and motion-ease-spring-bounciest give you spring physics without a JS runtime. They're implemented as cubic-bezier() approximations, which means they work anywhere CSS animations work — including on GPU-composited properties. That's a meaningful detail for performance.

Composing Complex Animations Without Keyframes

Where tailwindcss-motion really separates itself from plain Tailwind transition classes is in the composability story. You can layer a preset with individual property overrides when a preset isn't quite right for your use case.

// Fade in AND slide up simultaneously — no custom keyframes
<div className="
  motion-opacity-in-0
  motion-translate-y-in-[24px]
  motion-duration-[350ms]
  motion-ease-spring-smooth
">
  Card content
</div>

// Hover animation — scale up slightly on hover
<button className="
  hover:motion-scale-in-105
  hover:motion-duration-150
  transition-none
">
  Hover me
</button>

// Exit animation for a toast notification
<div
  className={
    isLeaving
      ? 'motion-opacity-out-0 motion-translate-x-out-[120%] motion-duration-200'
      : 'motion-preset-slide-left motion-duration-300'
  }
>
  {message}
</div>

The motion-translate-y-in-[24px] pattern — where you specify the *starting* transform value in brackets — is genuinely useful. You get the equivalent of a keyframe from { transform: translateY(24px) } without leaving your JSX. That 24px starting offset is a common value for card and modal entry animations; anything larger than around 32px starts feeling theatrical.

Look, some developers will hit the limits here. If you need multi-step animations — like an element that bounces, then rotates, then fades — you'll still need custom keyframes or a proper animation library. tailwindcss-motion isn't trying to replace GSAP. But for every single-phase entry, exit, and hover animation you build, this plugin eliminates the boilerplate. That covers the vast majority of real-world UI work.

One area worth calling out: combining tailwindcss-motion with Empire UI's component library works extremely well. Components like the glassmorphism card or neobrutalism button are unstyled enough that you can drop motion classes directly on the wrapper without fighting specificity issues. Add motion-preset-scale-up motion-duration-300 to an Empire UI glassmorphism card and it just works.

Reduced Motion and Accessibility

This is the section most animation tutorials skip. Don't skip it. A non-trivial percentage of your users have vestibular disorders or have explicitly requested reduced motion in their OS settings. The @media (prefers-reduced-motion: reduce) query exists for them, and ignoring it is an accessibility failure.

tailwindcss-motion handles this automatically for its preset classes — when prefers-reduced-motion: reduce is active, the plugin strips the animation entirely and just shows the final state. No flicker, no movement, content still visible. That behavior is baked in since v0.3.0.

// The plugin handles prefers-reduced-motion automatically.
// But if you're writing custom animation logic alongside it,
// guard it like this:
const prefersReducedMotion =
  typeof window !== 'undefined' &&
  window.matchMedia('(prefers-reduced-motion: reduce)').matches;

<div
  className={
    prefersReducedMotion
      ? '' // no animation
      : 'motion-preset-slide-up motion-duration-400'
  }
>
  {children}
</div>

In practice, the automatic handling is good enough for most projects. Where you need to be careful is when you use the low-level motion-translate-y-in-* classes directly — those don't get the same automatic suppression as the preset classes in some plugin versions. Test it. A quick check in Chrome DevTools with "Emulate CSS media feature prefers-reduced-motion" takes 30 seconds and will catch any regressions.

Using tailwindcss-motion with Empire UI Components

The combination of tailwindcss-motion and a component library that doesn't fight you on class overrides is genuinely productive. Empire UI is built specifically to be composable — you can pass className props down and add motion classes without touching any internals.

Here's a realistic pattern: a staggered grid of cards where each card slides up on mount. Pair this with Empire UI's neobrutalism or glassmorphism components and you get a polished, animated layout in about 15 lines.

import { GlassCard } from '@empire-ui/glass';

const features = [
  { title: 'Fast', desc: 'Renders in under 16ms' },
  { title: 'Typed', desc: 'Full TypeScript coverage' },
  { title: 'Accessible', desc: 'WCAG AA out of the box' },
];

export function FeaturesGrid() {
  return (
    <div className="grid grid-cols-3 gap-6">
      {features.map((f, i) => (
        <GlassCard
          key={f.title}
          className="motion-preset-slide-up motion-duration-500"
          style={{ '--motion-delay': `${i * 100}ms` } as React.CSSProperties}
        >
          <h3 className="text-lg font-semibold">{f.title}</h3>
          <p className="text-sm opacity-70">{f.desc}</p>
        </GlassCard>
      ))}
    </div>
  );
}

The stagger via CSS custom property --motion-delay is supported natively in the plugin — you don't need a JS loop managing setTimeout calls. Each card gets its own delay, they animate in sequence, and the whole thing works with zero JavaScript animation logic. If you want to push visual variety further, the gradient generator can help you build the gradient background that makes the glass cards pop.

When to Reach for Something Else

tailwindcss-motion isn't the answer to every animation problem. Know where its edges are so you don't paint yourself into a corner on a complex project.

Reach for Framer Motion when you need gesture-driven animation (drag, swipe), complex orchestration with AnimatePresence, or physics simulations that go beyond spring curves. Reach for GSAP when you need timeline control, scroll-triggered sequences, or animation of SVG paths. Reach for CSS custom `@keyframes` when you have a bespoke multi-step animation that doesn't map to any preset — it's still faster than a runtime library for a one-off effect.

That said, tailwindcss-motion covers a wide swath of what typical product UIs actually need: entry animations, hover feedback, loading states, exit transitions. Most SaaS landing pages, dashboards, and marketing sites could run entirely on this plugin without touching a single external animation library. That's a strong position to be in from a bundle-size and maintenance perspective.

Worth noting: you can mix tailwindcss-motion with Framer Motion in the same project — they don't conflict. A common pattern is using the plugin for static entry animations across the whole UI and Framer Motion only for interactive components that need gesture support. The box shadow generator at Empire UI uses layered effects that you can combine with motion classes to create buttons with both visual depth and animated entry. It's a small detail that elevates the perceived quality of any UI significantly.

FAQ

Does tailwindcss-motion work with Tailwind v4?

Yes. For v4 you register it with an @plugin directive in your CSS file instead of the plugins array in tailwind.config.ts. The utility class API is identical between v3 and v4.

Will tailwindcss-motion bloat my CSS bundle?

No — it tree-shakes with Tailwind's content scanning. Only the classes you use in your source files end up in the final CSS. Using three presets means three presets get generated, nothing more.

Does it respect prefers-reduced-motion?

Preset classes suppress all animation automatically when prefers-reduced-motion: reduce is active. If you use low-level property classes like motion-translate-y-in-*, test manually to confirm the behavior in your plugin version.

Can I use tailwindcss-motion and Framer Motion together?

Absolutely. They don't conflict. A common pattern is using the plugin for static entry animations and Framer Motion only for gesture-driven or orchestrated sequences where you actually need a runtime.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

tailwindcss-animate Plugin: Fade, Slide, Scale Entry AnimationsTailwind Scroll Animations: @starting-style and animation-timelineprefers-reduced-motion Guide: Safe Animations for Vestibular UsersCSS @starting-style: Entry Animations Without JS Classes