2026-04-05

Better understand Tailwind and animation with examples

This post is a hands-on tour of Tailwind CSS animations. You’ll learn:

  • When to use built-in utilities
  • How to compose animations with states and variants
  • How to create custom keyframes (two approaches)
  • How to use arbitrary values for fine-grained control
  • Practical patterns: loaders, skeletons, staggered lists, and scroll-triggered effects

1) Quick wins with built-in animation utilities

Tailwind ships with helpful defaults:

  • animate-spin
  • animate-bounce
  • animate-ping
  • animate-pulse

Examples:

<!-- Spinner -->
<div class="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div>

<!-- Bouncy CTA -->
<button class="px-4 py-2 rounded bg-indigo-600 text-white hover:bg-indigo-500 animate-bounce">Get Started</button>

<!-- Notification dot -->
<span class="relative inline-flex h-3 w-3">
  <span class="absolute inline-flex h-full w-full rounded-full bg-rose-500 opacity-75 animate-ping"></span>
  <span class="relative inline-flex rounded-full h-3 w-3 bg-rose-600"></span>
</span>

<!-- Subtle pulse for attention -->
<div class="px-3 py-2 rounded bg-amber-100 text-amber-800 animate-pulse">Syncing…</div>

Tips:

  • Keep bouncy or flashy animations to small, key elements.
  • Use them as temporary states (loading, notify) rather than permanent motion.

2) Compose animations with states (hover, focus, group)

You can start/stop animations with variants like hover:, focus:, and group-hover:.

<!-- Animate child only when card is hovered -->
<a href="#" class="group block rounded-lg border p-4 hover:shadow">
  <div class="flex items-center gap-3">
    <div class="w-8 h-8 rounded-full bg-sky-100 text-sky-700 grid place-items-center group-hover:animate-bounce">⚡</div>
    <div>
      <h3 class="font-semibold">Fast performance</h3>
      <p class="text-sm text-slate-600">Optimized for speed</p>
    </div>
  </div>
</a>

Accessibility:

  • Respect user preference with motion-safe: and motion-reduce: variants:
<!-- Only animate if user allows motion -->
<div class="motion-safe:animate-bounce motion-reduce:animate-none">...</div>

3) Custom animations (two approaches)

You’ll often want your own keyframes. You can define them in:

  • Tailwind config (recommended for reusability)
  • CSS via @layer (fast in a single file)

A) Tailwind config (extend)

tailwind.config.js:

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      keyframes: {
        wiggle: {
          '0%, 100%': { transform: 'rotate(-3deg)' },
          '50%': { transform: 'rotate(3deg)' },
        },
        'fade-in': {
          '0%': { opacity: '0' },
          '100%': { opacity: '1' },
        },
        'fade-in-up': {
          '0%': { opacity: '0', transform: 'translateY(8px)' },
          '100%': { opacity: '1', transform: 'translateY(0)' },
        },
      },
      animation: {
        wiggle: 'wiggle 1s ease-in-out infinite',
        'fade-in': 'fade-in .6s ease-out both',
        'fade-in-up': 'fade-in-up .6s ease-out both',
      },
    },
  },
  plugins: [],
}

Usage:

<button class="px-3 py-2 rounded bg-emerald-600 text-white hover:bg-emerald-500 animate-wiggle">Save</button>
<div class="animate-fade-in">Hello</div>
<div class="animate-fade-in-up">I slide in</div>

B) CSS with @layer

Useful if you don’t want to touch the config or need a quick prototype.

/* globals.css (imported by your app) */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer keyframes {
  @keyframes float {
    0% { transform: translateY(0) }
    50% { transform: translateY(-6px) }
    100% { transform: translateY(0) }
  }
}

@layer utilities {
  .animate-float { animation: float 2s ease-in-out infinite }
}
<div class="inline-block animate-float">🌊</div>

4) Arbitrary values for micro-control

Tailwind’s JIT lets you dial in precise animation parameters without new config keys.

  • Duration: [animation-duration:1.5s]
  • Delay: [animation-delay:250ms]
  • Timing: [animation-timing-function:cubic-bezier(.2,.8,.2,1)]
  • Fill mode: [animation-fill-mode:both]
  • Iterations: [animation-iteration-count:3] or infinite

If you defined a keyframe named fade-in, you can compose a one-off animation value:

<!-- keyframes fade-in must exist (via config or @layer) -->
<div class="animate-[fade-in_.8s_cubic-bezier(.2,.8,.2,1)_both]">Smooth</div>

5) Staggered list animation (no frameworks)

Use CSS variables for index-based delays.

@layer keyframes {
  @keyframes fade-in-up { 0% { opacity: 0; transform: translateY(6px) } 100% { opacity: 1; transform: translateY(0) } }
}
@layer utilities {
  .animate-fade-in-up { animation: fade-in-up .5s ease-out both }
}
<ul class="space-y-2">
  <li style="--i:0" class="opacity-0 [animation-delay:calc(var(--i)*120ms)] animate-fade-in-up">Item A</li>
  <li style="--i:1" class="opacity-0 [animation-delay:calc(var(--i)*120ms)] animate-fade-in-up">Item B</li>
  <li style="--i:2" class="opacity-0 [animation-delay:calc(var(--i)*120ms)] animate-fade-in-up">Item C</li>
</ul>

You can also bake the delay into a reusable class if you generate it via a loop or a small utility function in your template.


6) Two practical patterns

A) Accessible loader

<div role="status" aria-live="polite" class="flex items-center gap-3">
  <div class="w-6 h-6 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>
  <span class="text-slate-700">Loading…</span>
</div>

B) Skeleton with shimmer

@layer keyframes {
  @keyframes shimmer {
    0% { background-position: -200% 0 }
    100% { background-position: 200% 0 }
  }
}
<!-- Card skeleton -->
<div class="p-4 border rounded-md space-y-3">
  <div class="h-4 w-1/3 rounded bg-[linear-gradient(110deg,#e5e7eb_8%,#f3f4f6_18%,#e5e7eb_33%)] bg-[length:200%_100%] animate-[shimmer_1.2s_linear_infinite]"></div>
  <div class="h-3 w-3/4 rounded bg-[linear-gradient(110deg,#e5e7eb_8%,#f3f4f6_18%,#e5e7eb_33%)] bg-[length:200%_100%] animate-[shimmer_1.2s_linear_infinite]"></div>
  <div class="h-3 w-2/4 rounded bg-[linear-gradient(110deg,#e5e7eb_8%,#f3f4f6_18%,#e5e7eb_33%)] bg-[length:200%_100%] animate-[shimmer_1.2s_linear_infinite]"></div>
</div>

7) Trigger on scroll with a tiny script

Use IntersectionObserver to add an animation class when elements enter the viewport.

<ul id="features" class="grid sm:grid-cols-2 gap-4">
  <li class="opacity-0">Fast builds</li>
  <li class="opacity-0">Tiny bundles</li>
  <li class="opacity-0">A11y-first</li>
  <li class="opacity-0">Great DX</li>
</ul>

<script>
  const els = document.querySelectorAll('#features > li');
  const io = new IntersectionObserver((entries) => {
    for (const e of entries) {
      if (e.isIntersecting) {
        e.target.classList.add('animate-[fade-in-up_.5s_ease-out_both]');
        io.unobserve(e.target);
      }
    }
  }, { threshold: 0.15 });
  els.forEach((el, i) => {
    el.style.setProperty('--i', i);
    el.style.animationDelay = `calc(var(--i) * 120ms)`;
    io.observe(el);
  });
</script>

Note: Ensure the fade-in-up keyframes exist (from earlier examples).


8) Performance and polish

  • Prefer transform and opacity for smooth, GPU-friendly animations.
  • Avoid animating layout-affecting properties (like top/left/width/height) when possible.
  • Keep durations short (150–600ms) to feel snappy.
  • Use motion-reduce: to respect accessibility preferences.
  • If using many simultaneous animations, consider will-change: transform sparingly: [will-change:transform].

9) Cheatsheet

  • Built-ins: animate-spin, animate-bounce, animate-ping, animate-pulse.
  • Custom via config: extend keyframes and animation, then animate-<name>.
  • Custom via CSS: @layer keyframes + @layer utilities.
  • Fine-tune with arbitrary values: [animation-duration:...], [animation-delay:...], animate-[<keyframe>_<dur>_<timing>_<fill>_<count>].
  • Combine with variants: hover:, focus:, group-hover:, motion-safe:, motion-reduce:.

Wrap-up

Tailwind’s animation story is powerful because it’s composable: start with built-ins, add custom keyframes as needed, and finish with arbitrary values for precision. Use the snippets above as a toolbox for real-world loaders, skeletons, entrances, and subtle motion that respects users and performs well.