Tailwind CSS Handbook v4.3

Animation

Built-in animation utilities, custom keyframes with the --animate-* theme namespace, and how to keep motion accessible.

Animations run without a state change, on a loop or once on entry. Tailwind ships four and lets you register your own as theme tokens.

Built-in animations#

UtilityEffectTypical use
animate-spinContinuous rotationLoading spinners
animate-pingScale and fade outwardsNotification pulses
animate-pulseGentle opacity fadeSkeleton placeholders
animate-bounceVertical bounceScroll hints
animate-noneDisableOverriding at a breakpoint
Loading preview…

Custom animations#

Define keyframes and register the animation under --animate-*:

app.css
@import "tailwindcss";

@theme {
  --animate-wiggle: wiggle 1s ease-in-out infinite;
  --animate-fade-in: fade-in 0.3s ease-out;
  --animate-slide-up: slide-up 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes wiggle {
  0%, 100% { transform: rotate(-3deg); }
  50%      { transform: rotate(3deg); }
}

@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes slide-up {
  from { opacity: 0; transform: translateY(0.75rem); }
  to   { opacity: 1; transform: translateY(0); }
}
HTML
<div class="animate-slide-up">…</div>
<button class="hover:animate-wiggle">…</button>

Keyframes inside @theme#

You can also nest the keyframes in the theme block, which keeps the token and its definition together:

CSS
@theme {
  --animate-fade-in: fade-in 0.3s ease-out;

  @keyframes fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
}

Arbitrary animations#

For a one-off, skip the token:

HTML
<div class="animate-[fade-in_0.3s_ease-out]">…</div>

Underscores stand in for spaces.

Accessibility#

HTML
<div class="motion-safe:animate-slide-up">…</div>
<div class="animate-bounce motion-reduce:animate-none">…</div>

Or globally in your base layer:

CSS
@layer base {
  @media (prefers-reduced-motion: reduce) {
    *,
    ::before,
    ::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
    }
  }
}

Other requirements worth knowing:

  • Anything that flashes more than 3 times per second can trigger seizures. Never animate at that rate.
  • Content that moves for more than 5 seconds must be pausable if it accompanies other content (WCAG 2.2.2).
  • Announce loading state to assistive technology, not just visually:
HTML
<div role="status">
  <svg class="size-5 animate-spin" aria-hidden="true">…</svg>
  <span class="sr-only">Loading…</span>
</div>

Common patterns#

Skeleton loader#

Loading preview…

Live status dot#

HTML
<span class="relative flex size-3">
  <span class="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-75"></span>
  <span class="relative inline-flex size-3 rounded-full bg-emerald-500"></span>
</span>

Staggered list entrance#

HTML
<ul>
  <li class="animate-fade-in [animation-delay:0ms]">…</li>
  <li class="animate-fade-in [animation-delay:80ms]">…</li>
  <li class="animate-fade-in [animation-delay:160ms]">…</li>
</ul>

Add [animation-fill-mode:backwards] so delayed items start invisible rather than flashing.

Marquee#

CSS
@theme {
  --animate-marquee: marquee 24s linear infinite;

  @keyframes marquee {
    from { transform: translateX(0); }
    to   { transform: translateX(-50%); }
  }
}
HTML
<div class="overflow-hidden">
  <div class="flex w-max animate-marquee gap-8 motion-reduce:animate-none">…duplicated content…</div>
</div>

Duplicating the content and translating by -50% gives a seamless loop.

Progress bar#

HTML
<div class="h-1.5 w-full overflow-hidden rounded-full bg-gray-200">
  <div class="h-full rounded-full bg-indigo-600 transition-[width] duration-500" style="width: 62%"></div>
</div>

Transitions vs animations#

TransitionAnimation
Triggered byA property change (hover, class toggle)Simply existing
LoopsNoYes, with infinite
Intermediate stepsNo — start and end onlyYes, any number of keyframes
Right forInteraction feedbackLoading states, ambient motion

Common mistakes#

  • Animating layout properties. Keyframes that change width or top reflow on every frame. Use transform.
  • No reduced-motion fallback. An accessibility failure, not a polish item.
  • animate-pulse on real content. It is a placeholder effect; pulsing live text is distracting.
  • Infinite animation on many elements. Each one keeps the compositor awake and drains battery.
  • Spinner with no accessible name. Add role="status" and an sr-only label.
  • Entrance animations without animation-fill-mode. Delayed elements flash at full opacity before starting.