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.
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:
transitionanimates colours, opacity, shadow, transforms and filters together, with a default of 150ms and an ease-out style curve.transition-colors,transition-opacity,transition-shadowandtransition-transformlimit what animates, which is a good habit on busy screens.duration-*,ease-*anddelay-*control timing. Note thatdelay-*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.
| Class | What it does | Dashboard use |
|---|---|---|
animate-spin | Continuous rotation | Loading buttons, refresh icons |
animate-pulse | Fades to 50% opacity and back | Skeleton loaders |
animate-ping | Scales up and fades out, repeating | "Live" or new-notification dots |
animate-bounce | Bounces up and down | Scroll 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 want | Use |
|---|---|
| Smooth hover or focus change | transition duration-200 hover:... |
| Progress bar glide | transition-[width] duration-700 ease-out |
| Loading spinner | animate-spin |
| Skeleton loader | animate-pulse |
| Live notification dot | animate-ping |
| Custom animation | --animate-* and @keyframes in @theme |
| Stagger a React list | Inline animationDelay per item |
| Animate on first render | starting:opacity-0 transition |
Fade a hidden element in/out | transition-discrete |
| Respect reduced motion | motion-safe:animate-... |
| Directional entrance animations | tw-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.