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#
| Utility | Effect | Typical use |
|---|---|---|
animate-spin | Continuous rotation | Loading spinners |
animate-ping | Scale and fade outwards | Notification pulses |
animate-pulse | Gentle opacity fade | Skeleton placeholders |
animate-bounce | Vertical bounce | Scroll hints |
animate-none | Disable | Overriding at a breakpoint |
Loading preview…
HTML
<div class="grid grid-cols-2 gap-6 p-6 text-xs sm:grid-cols-4">
<div class="flex flex-col items-center gap-2">
<svg class="size-8 animate-spin text-indigo-600" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" class="opacity-25"/><path d="M12 2a10 10 0 0 1 10 10" stroke="currentColor" stroke-width="3" stroke-linecap="round"/></svg>
<span class="text-gray-600">spin</span>
</div>
<div class="flex flex-col items-center gap-2">
<span class="relative flex size-4"><span class="absolute inline-flex size-full animate-ping rounded-full bg-emerald-400 opacity-75"></span><span class="relative inline-flex size-4 rounded-full bg-emerald-500"></span></span>
<span class="text-gray-600">ping</span>
</div>
<div class="flex flex-col items-center gap-2">
<div class="h-8 w-16 animate-pulse rounded bg-gray-300"></div>
<span class="text-gray-600">pulse</span>
</div>
<div class="flex flex-col items-center gap-2">
<svg class="size-6 animate-bounce text-indigo-600" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" d="M12 5v14m0 0-6-6m6 6 6-6"/></svg>
<span class="text-gray-600">bounce</span>
</div>
</div>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…
HTML
<div class="mx-auto max-w-sm space-y-3 rounded-xl border border-gray-200 p-4">
<div class="flex items-center gap-3">
<div class="size-10 animate-pulse rounded-full bg-gray-200"></div>
<div class="flex-1 space-y-2">
<div class="h-3 w-1/3 animate-pulse rounded bg-gray-200"></div>
<div class="h-3 w-1/2 animate-pulse rounded bg-gray-200"></div>
</div>
</div>
<div class="h-3 animate-pulse rounded bg-gray-200"></div>
<div class="h-3 w-5/6 animate-pulse rounded bg-gray-200"></div>
</div>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#
| Transition | Animation | |
|---|---|---|
| Triggered by | A property change (hover, class toggle) | Simply existing |
| Loops | No | Yes, with infinite |
| Intermediate steps | No — start and end only | Yes, any number of keyframes |
| Right for | Interaction feedback | Loading states, ambient motion |
Common mistakes#
- Animating layout properties. Keyframes that change
widthortopreflow on every frame. Usetransform. - No reduced-motion fallback. An accessibility failure, not a polish item.
animate-pulseon 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 ansr-onlylabel. - Entrance animations without
animation-fill-mode. Delayed elements flash at full opacity before starting.
Related topics#
- Transitions — state-driven motion
- Transforms — the properties to animate
- Theme & Design Tokens — the
--animate-*namespace - States & Variants —
motion-safeandmotion-reduce