Tailwind ships four ready animations: animate-spin for loaders, animate-ping for notification dots, animate-pulse for skeleton screens and animate-bounce for scroll hints, plus animate-none to stop them. In v4 you add your own by declaring an --animate-name variable and its @keyframes inside @theme, which creates an animate-name class.
| Class | Value | Typical use |
|---|---|---|
| animate-spin | spin 1s linear infinite | Loading spinners |
| animate-ping | ping 1s cubic-bezier(0, 0, 0.2, 1) infinite | Live or unread badges |
| animate-pulse | pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite | Skeleton placeholders |
| animate-bounce | bounce 1s infinite | Scroll down arrows |
| animate-none | animation: none | Turning an animation off at a breakpoint or state |
Updated
Every built-in animation below is running live, next to the keyframes Tailwind generates for it. Then build a custom one: choose the motion, timing and repeat count and copy a ready @theme block for Tailwind v4, or the matching config for older v3 projects.
Each preview uses the real Tailwind class. Switch on the reduced motion preview to see motion-safe at work.
Pick a motion, duration, easing and repeat count, then copy the @theme block for Tailwind v4.
Each animate-* class sets animation to a theme variable; these are the default values and keyframes.
--animate-spin: spin 1s linear infinite;
--animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
--animate-bounce: bounce 1s infinite;
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes ping { 75%, 100% { transform: scale(2); opacity: 0; } }
@keyframes pulse { 50% { opacity: 0.5; } }
@keyframes bounce {
0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8, 0, 1, 1); }
50% { transform: none; animation-timing-function: cubic-bezier(0, 0, 0.2, 1); }
}This creates animate-wiggle and animate-fade-up. Use them as class names straight away.
@import "tailwindcss";
@theme {
--animate-wiggle: wiggle 1s ease-in-out infinite;
--animate-fade-up: fade-up 0.4s ease-out both;
@keyframes wiggle {
0%, 100% { transform: rotate(-3deg); }
50% { transform: rotate(3deg); }
}
@keyframes fade-up {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: none; }
}
}The older JavaScript config, for projects that have not moved to v4 yet.
module.exports = {
theme: {
extend: {
keyframes: {
wiggle: {
"0%, 100%": { transform: "rotate(-3deg)" },
"50%": { transform: "rotate(3deg)" },
},
},
animation: {
wiggle: "wiggle 1s ease-in-out infinite",
},
},
},
};Arbitrary values, a variable reference, and motion-safe so the movement respects the system setting.
<button class="motion-safe:animate-wiggle">Pick me</button>
<section class="motion-safe:animate-fade-up">Fades in on load</section>
<div class="animate-[wiggle_0.5s_ease-in-out_3]">Wiggles three times</div>
<div class="animate-(--my-animation)">Reads a CSS variable</div>
<svg class="size-5 motion-safe:animate-spin motion-reduce:hidden" aria-hidden="true">...</svg>Plain CSS equivalents of the custom classes above, so you can edit the timing and watch it change.
<style>
@keyframes wiggle { 0%, 100% { transform: rotate(-3deg) } 50% { transform: rotate(3deg) } }
@keyframes fade-up { from { opacity: 0; transform: translateY(12px) } to { opacity: 1; transform: none } }
@keyframes pulse { 50% { opacity: .5 } }
.wiggle { animation: wiggle 1s ease-in-out infinite; display: inline-block; padding: 8px 14px; background: #4F98FB; color: #fff; border-radius: 999px }
.fade { animation: fade-up .8s ease-out both; margin-top: 12px }
.skel { animation: pulse 2s cubic-bezier(.4, 0, .6, 1) infinite; margin-top: 12px }
.skel div { height: 10px; background: #cbd5e1; border-radius: 6px; margin: 6px 0 }
@media (prefers-reduced-motion: reduce) { .wiggle, .fade, .skel { animation: none } }
</style>
<span class="wiggle">Pick me</span>
<p class="fade">This line fades up once.</p>
<div class="skel"><div style="width:60%"></div><div></div><div style="width:80%"></div></div>
Spin rotates an element a full turn every second and suits circular loaders. Ping scales a copy of a dot up to twice its size while fading it out, which reads as a radar pulse; put it on an absolutely positioned span behind a solid dot.
Pulse dips opacity to half and back over two seconds, the standard look for grey skeleton blocks while content loads. Bounce nudges an element up by a quarter of its height and drops it back, which works for a single arrow inviting people to scroll, and rarely anywhere else.
Tailwind v4 moved configuration into CSS. Any variable in the --animate-* namespace inside @theme becomes an animate-* utility, and @keyframes written inside the same block are emitted alongside it. Nothing else needs registering.
Keep the name in the variable and the name in @keyframes identical. A mismatch produces a class that sets an animation pointing at keyframes that do not exist, so nothing moves and no error appears.
Large, repeating or parallax movement can make some people feel unwell. Operating systems expose a reduce motion setting, and browsers pass it to CSS as prefers-reduced-motion.
Loaders that communicate status can keep a gentle version, while decorative bounces and attention grabbers should stop. Test with the setting switched on, or with the emulation option in your browser's developer tools.
Inside @theme in your main CSS file, declare --animate-wiggle: wiggle 1s ease-in-out infinite; and put the @keyframes wiggle rule in the same @theme block. Tailwind then generates an animate-wiggle class you can use anywhere.
v4 is configured in CSS, so theme.extend.keyframes and theme.extend.animation become @keyframes and --animate-* variables inside @theme. A JavaScript config still works if you load it with the @config directive, which helps during a migration.
Leave infinite out of the value, and add forwards if the element should stay at the final frame, for example --animate-fade-in: fade-in 0.3s ease-out forwards. For a one-off you can also write animate-[fade-in_0.3s_ease-out_forwards].
No. The motion-safe: and motion-reduce: variants map to the prefers-reduced-motion media query, but you have to apply them. A common pattern is motion-safe:animate-bounce, which only animates for people who have not asked for reduced motion.
Related: CSS animation generator, Respecting prefers-reduced-motion, CSS transitions playground, Scroll animations.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.