Tailwind Animations: Built-in Classes and Custom Keyframes

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.

Built-in animation utilities in Tailwind v4
ClassValueTypical use
animate-spinspin 1s linear infiniteLoading spinners
animate-pingping 1s cubic-bezier(0, 0, 0.2, 1) infiniteLive or unread badges
animate-pulsepulse 2s cubic-bezier(0.4, 0, 0.6, 1) infiniteSkeleton placeholders
animate-bouncebounce 1s infiniteScroll down arrows
animate-noneanimation: noneTurning 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.

The built-in animations, live

Each preview uses the real Tailwind class. Switch on the reduced motion preview to see motion-safe at work.

Keyframe builder

Pick a motion, duration, easing and repeat count, then copy the @theme block for Tailwind v4.

The CSS behind the built-in classes

Keyframes Tailwind v4 generates

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); }
}

Custom animations in v4 and v3

Tailwind v4: @theme in your CSS

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; }
  }
}

Tailwind v3: tailwind.config.js

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",
      },
    },
  },
};

Using them in markup

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>

Custom keyframes running live

Wiggle, fade up and a skeleton card

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>

What each built-in animation is for

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.

Custom keyframes in v4

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.

Motion and accessibility

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.

How it works

  1. Watch the four built-in animations and copy the class you need.
  2. Toggle the reduced motion preview to see what motion-safe and motion-reduce do.
  3. Use the keyframe builder to pick a motion, duration, easing and repeat count.
  4. Paste the generated @theme block into the CSS file that imports Tailwind, then use the new class.

Tips

Frequently asked questions

How do I add a custom animation in Tailwind v4?

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.

Where did tailwind.config.js keyframes go in v4?

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.

How do I make an animation run once?

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].

Does Tailwind respect reduced motion settings automatically?

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.

More in Guides

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.

Product

Who It's For

Features

Resources

Company

View Sitemap