EmpireUI
Get Pro
← Blog8 min read#tailwind animations#css#transitions

Tailwind CSS Animations: Every Built-In + How to Add Custom

Tailwind CSS ships with four built-in animations that cover 90% of UI needs. Here's how to use every one, then extend them with your own custom keyframes.

Developer writing CSS animation code on a laptop screen at night

The Four Built-In Animations You Actually Have

Tailwind v3 ships with exactly four utility animations out of the box: animate-spin, animate-ping, animate-pulse, and animate-bounce. That's it. Don't expect a library of 40 effects — that's not what Tailwind is for. What you get is enough to handle loading states, attention indicators, and subtle UI feedback without writing a single line of raw CSS.

Here's the quick-reference breakdown. animate-spin rotates an element continuously at 1 full rotation per second — classic for loading spinners. animate-ping scales an element up while fading it out, which is that ripple effect you see on notification badges. animate-pulse fades between full opacity and 50% opacity, perfect for skeleton loaders. animate-bounce does exactly what the name says, a vertical bounce using a custom easing curve.

Honestly, animate-pulse is the one you'll reach for constantly. Skeleton loading screens went mainstream around 2019 and they've stuck. Slap it on a gray div and you've got a professional loading state in seconds.

One more thing — all four of these respect prefers-reduced-motion if you pair them with Tailwind's motion-reduce: variant. That's motion-reduce:animate-none and you're done. Accessibility sorted with four words.

Transitions: The Often-Confused Sibling

Animations and transitions are different tools. animate-* classes run indefinitely or for a set iteration count. transition-* classes react to state changes — hover, focus, active. You need both in your toolkit and you need to know which one to reach for.

Tailwind's transition utilities are transition, transition-all, transition-colors, transition-opacity, transition-shadow, and transition-transform. Pair any of them with duration-* and ease-* classes. A button that smoothly changes background on hover takes three classes: transition-colors duration-200 ease-in-out.

Worth noting: the default transition class only transitions color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, and transform. If you're animating width or height, you want transition-all — or more precisely, you want to think twice before animating layout properties at all since they trigger reflow.

In practice, duration-150 and duration-200 cover 80% of UI interactions. Anything longer than 300ms starts feeling sluggish on interactive elements. Save the longer durations for page transitions and modals.

Adding Custom Animations in tailwind.config.js

This is where it gets interesting. Custom animations in Tailwind live in two places inside your config: theme.extend.keyframes and theme.extend.animation. You define the keyframes, then register the animation name with its timing properties. Tailwind generates the class automatically.

Here's a working example — a fade-in-up animation you'd use on modal content or cards entering the viewport:

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      keyframes: {
        'fade-in-up': {
          '0%': {
            opacity: '0',
            transform: 'translateY(16px)',
          },
          '100%': {
            opacity: '1',
            transform: 'translateY(0)',
          },
        },
        'shimmer': {
          '0%': { backgroundPosition: '-400px 0' },
          '100%': { backgroundPosition: '400px 0' },
        },
      },
      animation: {
        'fade-in-up': 'fade-in-up 0.3s ease-out',
        'shimmer': 'shimmer 1.4s ease-in-out infinite',
      },
    },
  },
};

Once that's in your config, you get animate-fade-in-up and animate-shimmer as utility classes. Use them exactly like the built-ins. The shimmer one is particularly useful if you want a more polished skeleton loader than animate-pulse gives you — it mimics the moving highlight effect you see on high-end loading states.

Quick aside: if you're on Tailwind v4 (released in 2025), the config format changed significantly. Custom animations now go in your CSS file using @theme blocks instead of tailwind.config.js. Same concept, different syntax — check the v4 migration docs before copying config examples blindly.

Using Animations with Variants: Hover, Group, and Responsive

You can scope any animation to a state. hover:animate-spin on an icon, group-hover:animate-bounce on a child element, focus:animate-ping on a form input indicator — all of it works with Tailwind's variant system. This unlocks interaction patterns without touching JavaScript at all.

Look, the group variant deserves more attention than it gets. If you have a card component and you want a child arrow icon to animate when the user hovers the whole card, wrap the card in group and use group-hover:animate-bounce on the arrow. The parent captures the hover, the child reacts. Clean pattern, zero JS.

<div className="group relative overflow-hidden rounded-xl p-6 bg-white shadow-md cursor-pointer">
  <h3 className="text-lg font-semibold">Card Title</h3>
  <p className="text-gray-500 mt-2">Some card description text here.</p>
  <span className="inline-block mt-4 transition-transform duration-200 group-hover:translate-x-1">
    Learn more →
  </span>
</div>

Responsive variants work too. You could have md:animate-none animate-pulse if you want the skeleton pulse only on mobile. Niche, but valid. The variant system is composable all the way down.

Animation Delay and Iteration Count: The Missing Utilities

Here's a gap that trips people up — Tailwind doesn't ship animation-delay or animation-iteration-count utilities by default. You'll hit this the first time you try to stagger a list of items fading in. The classes don't exist. You have a few options.

The most pragmatic approach for one-off delays is an inline style: style={{ animationDelay: '150ms' }}. Ugly? Slightly. Does it work? Perfectly. For a list of four items with staggered entry animations, you'd map over them and multiply the index by 75 or 100ms. That's 0ms, 75ms, 150ms, 225ms — a clean cascade.

If you want it in Tailwind properly, extend your config with arbitrary delay values or use the [150ms] arbitrary value syntax if you're on v3.2+: [animation-delay:150ms]. That's the bracket notation for one-off values that don't belong in your design system.

For iteration count, same story. [animation-iteration-count:3] runs the animation three times and stops. Combine it with animation-fill-mode: forwards via [animation-fill-mode:forwards] and the element stays in the final state after the animation completes. This matters for entrance animations — you don't want your card to snap back to invisible after fading in.

The components on Empire UI handle a lot of this opinionated wiring for you, especially the motion-heavy UI blocks. Worth checking what's already built before rolling your own stagger logic.

Performance: What You Should and Shouldn't Animate

Not all CSS properties are equal when it comes to animation performance. The compositor-friendly properties are transform and opacity. Animate those and the browser can do the work entirely on the GPU without triggering layout or paint. Animate width, height, top, left, margin or anything that affects layout and you're triggering reflow on every frame — that's where jank comes from.

The practical upshot: if you want to move something, use transform: translateX() not left. If you want to show/hide, animate opacity not display. Tailwind's translate-*, scale-*, and rotate-* utilities all compile down to transform properties, so you're safe. The box shadow generator on Empire UI actually previews shadow transitions in real time — useful for checking if your shadow animation feels smooth before committing.

One more thing — will-change: transform is a hint to the browser to promote an element to its own compositor layer before the animation starts. Add it via Tailwind's will-change-transform utility when you have an element you know will animate. Don't slap it on everything though; each promoted layer consumes GPU memory. Use it surgically on elements that animate on interaction.

If you're building rich UI with lots of motion — glassmorphism cards, aurora gradients, that kind of thing — the glassmorphism generator gives you the CSS output with transition properties already tuned for smooth rendering. Saves the performance debugging loop.

Putting It All Together: A Practical Component Pattern

Let's wire everything into a real component. An animated notification badge with a ping effect, a card that fades in on mount, and a button with a hover transition — all Tailwind, no custom CSS file needed.

import { useEffect, useState } from 'react';

export function AnimatedCard({ title, description, badge }) {
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    // Tiny timeout so the initial render triggers the animation
    const t = setTimeout(() => setVisible(true), 10);
    return () => clearTimeout(t);
  }, []);

  return (
    <div
      className={[
        'relative p-6 bg-white rounded-2xl shadow-lg transition-all duration-300',
        visible
          ? 'opacity-100 translate-y-0'
          : 'opacity-0 translate-y-4',
      ].join(' ')}
    >
      {/* Ping badge */}
      {badge && (
        <span className="absolute top-4 right-4 flex h-3 w-3">
          <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75" />
          <span className="relative inline-flex rounded-full h-3 w-3 bg-sky-500" />
        </span>
      )}

      <h3 className="text-xl font-semibold text-gray-900">{title}</h3>
      <p className="mt-2 text-gray-500">{description}</p>

      <button className="mt-4 px-4 py-2 bg-gray-900 text-white rounded-lg transition-colors duration-150 hover:bg-gray-700 active:scale-95 transition-transform">
        Open
      </button>
    </div>
  );
}

The fade-in uses a CSS transition triggered by a class swap, not a keyframe animation — because it only needs to run once. The ping badge uses animate-ping from Tailwind's built-ins. The button gets transition-colors for the hover and active:scale-95 for the press feedback. Three different animation techniques, all idiomatic Tailwind.

That's the real pattern: reach for transitions on state changes, reach for keyframe animations on looping or one-shot sequences, and keep your JS minimal. Tailwind's utilities make that separation natural rather than something you have to enforce with discipline.

FAQ

Does Tailwind CSS support CSS animations out of the box?

Yes, but only four: animate-spin, animate-ping, animate-pulse, and animate-bounce. For anything else you extend the config with custom keyframes.

How do I add animation delay in Tailwind?

There's no built-in delay utility. Use arbitrary values like [animation-delay:200ms] in Tailwind v3.2+, or just inline styles for one-offs. Neither is pretty, but both work fine.

What's the difference between animate-pulse and animate-ping?

animate-pulse fades opacity back and forth — good for skeleton loaders. animate-ping scales the element up while fading it out, giving you the ripple/sonar effect for notification dots.

Will Tailwind animations hurt performance?

Only if you animate layout properties like width or height. Stick to transform and opacity — which most Tailwind motion utilities compile to — and you'll stay off the main thread.

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

Read next

Tailwind CSS v4: Every New Feature Worth Knowing AboutTailwind CSS Mastery: Every Utility, Plugin, and Pattern in One GuideCSS View Transitions Advanced: Cross-Document, Custom AnimationsHow to Add a Custom Cursor in React (Free Tailwind Guide)