Frontend

Tailwind CSS Animations: You Probably Don't Need a Motion Library for Your Dashboard

Most dashboard animations don't need GSAP or Framer Motion. Tailwind's built-in transitions and custom keyframes handle skeleton loaders, spinners, and entrance animations without the extra dependency.

·8 min read·Samson Ameh
#Tailwind#CSS#React#Animation#Performance

Tailwind CSS Animations: You Probably Don't Need a Motion Library

A lot of React projects pull in a full animation library the moment someone says "can we make it feel a bit smoother?". For a simple dashboard, that is often more than the job needs. A skeleton loader, a spinning button, a card that fades in, a progress bar that glides to its new value: Tailwind can do all of that with classes you already have, and no extra dependency.

This post covers what Tailwind v4 gives you out of the box, how to add your own animation in a few lines of CSS, and an honest list of where you really should reach for a library like Motion.

1. Transitions: the 80% case

Most "animation" in a dashboard is really a transition between two states: hover, focus, selected, open. For that you only need the transition-* utilities.

jsx
<div className="rounded-xl border bg-white p-4 shadow-sm transition duration-200 hover:-translate-y-0.5 hover:shadow-md">
  ...
</div>

The useful pieces:

  • transition animates colours, opacity, shadow, transforms and filters together, with a default of 150ms and an ease-out style curve.
  • transition-colors, transition-opacity, transition-shadow and transition-transform limit what animates, which is a good habit on busy screens.
  • duration-*, ease-* and delay-* control timing. Note that delay-* delays transitions, not keyframe animations.
  • transition-[width] (any arbitrary property) works too.

A dashboard staple, a progress bar that glides when its value changes:

jsx
<div className="h-2 w-full rounded-full bg-gray-200">
  <div
    className="h-2 rounded-full bg-indigo-500 transition-[width] duration-700 ease-out"
    style={{ width: `${percent}%` }}
  />
</div>

When percent changes in state, the bar animates to the new width.

2. The built-in animate-* utilities

Tailwind ships four ready-made keyframe animations that map neatly onto common dashboard needs.

ClassWhat it doesDashboard use
animate-spinContinuous rotationLoading buttons, refresh icons
animate-pulseFades to 50% opacity and backSkeleton loaders
animate-pingScales up and fades out, repeating"Live" or new-notification dots
animate-bounceBounces up and downScroll hints, attention nudges

A loading button:

jsx
<button disabled className="inline-flex items-center rounded-md bg-indigo-500 px-4 py-2 text-white">
  <svg className="mr-3 size-5 animate-spin" viewBox="0 0 24 24">{/* spinner icon */}</svg>
  Processing…
</button>

A skeleton card while data loads:

jsx
<div className="animate-pulse space-y-3 rounded-xl border p-4">
  <div className="h-4 w-1/3 rounded bg-gray-200" />
  <div className="h-8 w-1/2 rounded bg-gray-200" />
  <div className="h-3 w-full rounded bg-gray-200" />
</div>

A "live" indicator:

jsx
<span className="relative flex size-3">
  <span className="absolute inline-flex size-full animate-ping rounded-full bg-green-400 opacity-75" />
  <span className="relative inline-flex size-3 rounded-full bg-green-500" />
</span>

You can also use arbitrary values for one-offs, such as animate-[wiggle_1s_ease-in-out_infinite], or point at a CSS variable with animate-(--my-animation).

3. Add your own animation in a few lines of CSS

In Tailwind v4 you define custom animations inside @theme: a --animate-* variable plus the @keyframes it uses. A class is generated automatically.

css
@import "tailwindcss";

@theme {
  --animate-fade-in-up: fade-in-up 0.4s ease-out both;

  @keyframes fade-in-up {
    from {
      opacity: 0;
      transform: translateY(8px);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }
}

Now animate-fade-in-up works anywhere. The both at the end keeps the element hidden before a delayed animation starts and visible after it finishes, which matters for staggering.

Staggered entrance for React dashboard cards

Here's the gotcha: delay-* only affects transitions, not keyframe animations. Set animation-delay with an inline style, using the index from your .map():

jsx
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
  {stats.map((stat, i) => (
    <div
      key={stat.id}
      className="rounded-xl border bg-white p-4 motion-safe:animate-fade-in-up"
      style={{ animationDelay: `${i * 75}ms` }}
    >
      <p className="text-sm text-gray-500">{stat.label}</p>
      <p className="text-2xl font-semibold">{stat.value}</p>
    </div>
  ))}
</div>

That's a polished staggered load-in with no library and a few lines of CSS.

You can override the built-ins the same way: redefining --animate-spin in @theme changes what animate-spin does everywhere.

4. The starting: variant for enter transitions

Animating an element when it first appears used to push people toward libraries. Tailwind v4 has a starting variant, built on the CSS @starting-style rule, which sets how an element looks on first render or when it goes from display: none to visible.

jsx
<div className="translate-y-0 opacity-100 transition duration-300 starting:translate-y-2 starting:opacity-0">
  Saved successfully
</div>

When this mounts, it starts transparent and slightly lower, then transitions to its normal state. It's a good fit for toasts, banners and popovers. Support for @starting-style varies by browser, so check caniuse or MDN before relying on it.

5. Animating hidden to visible: transition-discrete

By default, a transition doesn't run when a property flips between discrete values like display: none and display: block. Tailwind's transition-discrete sets transition-behavior: allow-discrete, which lets it run.

jsx
<label className="peer">
  <input type="checkbox" checked />
</label>
<button className="hidden transition-all transition-discrete not-peer-has-checked:opacity-0 peer-has-checked:block">
  I fade out
</button>

transition-normal is the default and turns it back off.

6. Respect reduced motion

Some users ask their OS to reduce motion, and dashboard animations should respect that. Tailwind makes it one prefix:

  • motion-safe: applies the style only if the user has not asked for reduced motion.
  • motion-reduce: applies only if they have.
jsx
<svg className="size-5 motion-safe:animate-spin" />

A good default: put motion-safe: on decorative animations, and leave functional ones like a spinner with an alternative such as showing text.

7. Ready-made entrance classes with tw-animate-css (separate package)

If you want a larger set of enter and exit style classes without writing keyframes, tw-animate-css is a small CSS-only package for Tailwind v4. Note: this is a separate package, not built into Tailwind.

Terminal
$npm install -D tw-animate-css

Then import it in your CSS:

css
@import "tailwindcss";
@import "tw-animate-css";

Now you get classes like animate-in, fade-in, zoom-in and slide-in-from-top-8:

jsx
<div className="animate-in fade-in slide-in-from-bottom-4 duration-500">...</div>

It describes itself as a v4-compatible replacement for the older tailwindcss-animate plugin, but covers only part of that plugin, so it may not be a drop-in swap. Pin your version since v2.0.0 will have breaking changes.

8. Where Tailwind stops, and a library earns its place

Tailwind animation is CSS, and CSS has real limits. Reach for Motion (Framer Motion or Motion-dom) or a similar library when you need:

  • Exit animations for React components. When React unmounts an element, it's gone immediately. You can work around this by toggling a class, but it gets awkward fast.
  • Layout animations, where items smoothly reflow when a list reorders or filters.
  • Gestures and drag, such as draggable cards or swipe to dismiss.
  • Spring physics and interruptible animations that respond to user input.
  • Orchestrated sequences across many elements that depend on each other.

If your dashboard only needs loading states, hover feedback, entrance animations and toasts, Tailwind alone covers it. When you hit exit animations, layout animation or gestures, add a motion library, because at that point it does something CSS cannot.

Quick reference cheat sheet

You wantUse
Smooth hover or focus changetransition duration-200 hover:...
Progress bar glidetransition-[width] duration-700 ease-out
Loading spinneranimate-spin
Skeleton loaderanimate-pulse
Live notification dotanimate-ping
Custom animation--animate-* and @keyframes in @theme
Stagger a React listInline animationDelay per item
Animate on first renderstarting:opacity-0 transition
Fade a hidden element in/outtransition-discrete
Respect reduced motionmotion-safe:animate-...
Directional entrance animationstw-animate-css package (separate)

Wrapping up

For a React dashboard, Tailwind gives you transitions, four built-in animations, custom keyframes and enter transitions, with reduced-motion support in one prefix. Start there. When you hit exit animations, layout animation or gestures, add a motion library, because at that point it does something CSS cannot.

Written by

Samson Ameh

Frontend Engineer specialising in React, TypeScript, and enterprise UI systems. Based in Lagos, Nigeria.