GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 Tailwind v4 Animations — One Line, Copy-Paste

These nine Tailwind v4 animations turn one utility class into a real keyframe, a hover state, or a reduced-motion guard, using only the framework's

Auto-plays · click a tile to jump to its section · all nine in one zip

The nine below move from registering a brand-new animation to extending one Tailwind already ships. 01 through 03 cover the CSS-first way to declare a custom keyframe inside @theme, and 04 shows how to fake a feature browsers still disagree on. 05 and 06 react to hover and to a JavaScript-driven state, 07 is the accessibility guard on its own, and 08 and 09 stretch a built-in utility instead of writing a new one. Only the Tailwind fragment sits in the code block below each entry — the compiled CSS, the plain-SCSS twin, and a React version of all nine live together in one archive. If you would rather reach the same result with no utility classes at all, the particle backgrounds walk that road in plain CSS, and when only one line of text needs the emphasis, the text highlight animations are where this @theme grammar goes next.

01@theme Custom Keyframes

Why a tailwind v4 custom animation lives in CSS

Writing an --animate-pop-spin variable together with its @keyframes inside a CSS @theme block turns that name straight into an animate-pop-spin utility class — no tailwind.config.js required.

@theme custom keyframes
Open in a new tab

01 · @theme custom keyframes — Measured area 2.196% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password

cubic-bezier 오버슈트@keyframes 동봉900ms

Where a one-shot badge fits

It suits a badge that only needs to spin into view once, such as a new-item tag or an updated notification count.

<style>
@theme {
  --animate-pop-spin: pop-spin 900ms cubic-bezier(.34,1.56,.64,1) both;
  @keyframes pop-spin {
    0%   { opacity: 0; transform: scale(.5) rotate(-8deg); }
    60%  { opacity: 1; transform: scale(1.08) rotate(3deg); }
    100% { opacity: 1; transform: scale(1) rotate(0deg); }
  }
}
</style>
<span class="animate-pop-spin motion-reduce:animate-none rounded-full bg-[#fff7e6] px-4 py-2 font-bold">NEW</span>

02tw-animate-css Entrance Utility

One plugin combination, one card

Installing the tw-animate-css plugin and applying its animate-in fade-in slide-in-from-top-8 duration-500 combination makes a card start 32px higher and fully transparent, then settle into place while it fades in.

tw-animate-css entrance utility
Open in a new tab

02 · tw-animate-css entrance utility — Measured area 27.637% · moving frames 9/23Original by GODRICH · SCSS and React ship in the zip · See the password

@keyframes slide-intranslateY(-32px)500ms

A toast or a modal's first appearance

It fits a toast or a modal's very first appearance, or a card sliding into a list.

npm install -D tw-animate-css

<!-- add one line to app.css: @import "tw-animate-css"; -->
<div
  class="animate-in fade-in slide-in-from-top-8 duration-500
         motion-reduce:animate-none
         rounded-2xl bg-[#17141a] p-6"
>
  <p class="text-[#fff7e6] font-bold">Deployed</p>
  <p class="text-[#fff7e6]/70 text-sm">The new version just went live</p>
</div>

03Fade + Slide Utility

Opacity and translateY in one keyframe

Defining just one --animate-fade-slide variable inside @theme — no plugin needed — bundles opacity and translateY into a single keyframe, so a paragraph rises 16px into view with one class.

Fade + slide utility
Open in a new tab

03 · Fade + slide utility — Measured area 6.125% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password

@keyframes fade-slidetranslateY+opacity400ms

A hero line settling into place

It works well for a hero line or a section title that should feel like it's settling into place.

<style>
@theme {
  --animate-fade-slide: fade-slide 400ms ease-out both;
  @keyframes fade-slide {
    from { opacity: 0; transform: translateY(16px); }
    to   { opacity: 1; transform: translateY(0); }
  }
}
</style>
<p class="animate-fade-slide text-center font-bold text-2xl">One line is all it takes</p>

04Arbitrary-Value scroll-timeline

An arbitrary property instead of a utility

Tailwind ships no dedicated scroll-timeline utility, so it gets attached through the arbitrary property [animation-timeline:scroll()] instead.

Arbitrary-value scroll-timeline
Open in a new tab

04 · Arbitrary-value scroll-timeline — Measured area 1.661% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

@keyframes fill-loop임의 속성caniuse 확인

Why the demo fakes the progress

Because browser support still varies, this demo reproduces the same fill progress with a safe looping animation rather than the real scroll-driven one.

<!-- only in supporting browsers — check caniuse.com before shipping this line -->
<div class="h-2 rounded-full bg-black/10">
  <div
    class="h-full rounded-full bg-[#2f6df6] origin-left
           animate-[fill_1s_linear] [animation-timeline:scroll()] [animation-range:0%_100%]"
  ></div>
</div>

05group-hover Icon

group on the parent, translate on the icon

Putting group on the parent and group-hover:translate-x-1 on the icon means hovering anywhere on the card slides just the inner arrow 4px to the right.

group-hover icon
Open in a new tab

05 · group-hover icon — Measured area 0.098% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · See the password

.card:hover 자식translateX(4px)200ms

A card CTA or a list row arrow

It's a light nudge that suits a card-style call to action or an arrow sitting at the end of a list row.

<a class="group inline-flex items-center gap-3 rounded-xl bg-white/8 px-6 py-4" href="/more">
  <span class="text-[#fff7e6] font-semibold">Learn more</span>
  <svg class="size-4 text-[#2f6df6] transition-transform duration-200
              group-hover:translate-x-1 group-focus-visible:translate-x-1" viewBox="0 0 24 24">
    <path d="M4 12h14M13 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2" />
  </svg>
</a>

06data-state Animation (shadcn)

A script that only flips one attribute

Like shadcn's own components, a small script only toggles data-state="open" on click, and CSS reacts through the data-[state=open]: variant to drive scale-y-100 and opacity-100.

data-state animation (shadcn)
Open in a new tab

06 · data-state animation (shadcn) — Measured area 14.289% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password

[data-state=open]scaleY(1)attr 토글 JS

Opening without a layout recalculation

Nothing here touches width, height, or grid-template-rows, so the panel opens without forcing a layout recalculation.

<button onclick="p.dataset.state = p.dataset.state === 'open' ? 'closed' : 'open'"
        aria-expanded="false" class="w-full rounded-xl bg-[#17141a] px-5 py-4 text-left text-[#fff7e6]">
  Shipping details
</button>
<div id="p" data-state="closed"
     class="origin-top scale-y-0 opacity-0 transition-all duration-[220ms] ease-out
            data-[state=open]:scale-y-100 data-[state=open]:opacity-100">
  <p>Orders ship within one to two business days after checkout.</p>
</div>

07motion-safe Guard

Two variants, no media query

Pairing motion-safe:hover:-translate-y-1 with motion-reduce:hover:shadow-md needs no separate @media block at all.

motion-safe guard
Open in a new tab

07 · motion-safe guard — Measured area 3.297% · moving frames 8/23Original by GODRICH · SCSS and React ship in the zip · See the password

prefers-reduced-motion: no-preferencereduce 는 box-shadow 만220ms

What a reduced-motion visitor sees

A reduced-motion visitor only sees the shadow soften, because the lift itself is never attached in the first place.

<button
  class="rounded-xl bg-[#fff7e6] px-8 py-4 font-bold shadow-md transition-all duration-[220ms] ease-[cubic-bezier(0.2,0.8,0.2,1)]
         motion-safe:hover:-translate-y-1 motion-safe:hover:shadow-lg
         motion-reduce:hover:shadow-md"
>
  Save changes
</button>

08Spin Utility

Extending a built-in instead of replacing it

Leaving the built-in animate-spin (one second) untouched and adding just --animate-spin-slow: spin 3s linear infinite inside @theme lets a logo and a loading ring share the exact same rotation at two different speeds.

Spin utility
Open in a new tab

08 · Spin utility — Measured area 1.432% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

@keyframes spinrotate(360deg) 공유linear infinite

One keyframe, two speeds

Neither utility needs its own @keyframes block, since both point back to the same spin keyframe Tailwind already ships.

<style>
@theme {
  --animate-spin-slow: spin 3s linear infinite;
}
</style>
<div class="flex items-center gap-8">
  <span class="animate-spin size-14 rounded-full border-4 border-black/10 border-t-[#2f6df6]"></span>
  <span class="animate-spin-slow size-14 rounded-full border-4 border-black/10 border-t-[#17141a]"></span>
</div>

09Stagger Utility

One shared class, one changing variable

Giving each list item [animation-delay:calc(var(--i)*80ms)] and simply changing --i to 0, 1, 2 and so on makes one shared animate-fade-slide class rise in, each item landing 80ms after the last.

Stagger utility
Open in a new tab

09 · Stagger utility — Measured area 20.843% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · See the password

var(--i)animation-delay 계산80ms 간격

A sequence instead of an instant

It reads as a sequence instead of everything appearing on screen at once.

<ul class="flex flex-col gap-2">
  <li style="--i:0" class="animate-fade-slide [animation-delay:calc(var(--i)*80ms)] rounded-lg bg-white/8 px-4 py-2">
    New comment on your post
  </li>
  <li style="--i:1" class="animate-fade-slide [animation-delay:calc(var(--i)*80ms)] rounded-lg bg-white/8 px-4 py-2">
    You have a new follower
  </li>
  <li style="--i:2" class="animate-fade-slide [animation-delay:calc(var(--i)*80ms)] rounded-lg bg-white/8 px-4 py-2">
    Payment completed
  </li>
</ul>

Where Do Tailwind v4 Animations Break?

v4 never reads the config file

Folding with a transform instead of a layout property matters more as layers pile up — in the paper cutout landing kit, where sections sit several sheets deep, that single choice decides whether the page still scrolls smoothly.

The most common tailwind v4 animation not working report comes from the way up from v3. Leaving theme.extend.keyframes and theme.extend.animation inside tailwind.config.js and just swapping a class name to animate-pop-spin does nothing at all, because v4 is CSS-first and never reads that file. The official theme documentation is explicit that the definition has to live inside a CSS @theme block instead. Item 06's accordion hides a related trap: the usual tailwind animate height route, moving grid-template-rows from 0fr to 1fr, looks like the obvious way to open a panel, but it forces the browser to recompute the track size on every frame. That fights the transform-and-opacity approach the rest of this set relies on, so this article swaps in scaleY with transform-origin: top to sidestep the recalculation. The trade-off is that the panel's layout space still stays reserved either way. Item 08's second ring carries a smaller version of the same lesson: writing it a second @keyframes block would only grow the compiled CSS. Overriding just animation-duration on the shared spin keyframe is all a second speed needs. Every one of these nine, wired up with its SCSS twin and a React version, sits in the download and opens with mtsjkxrf typed exactly as it appears on this line, with no spaces added anywhere.

Accessibility

Two ways to answer reduced motion

All nine handle prefers-reduced-motion, but not with the same technique. Items 01 through 04, 06, 08, and 09 pair their custom keyframe with motion-reduce:animate-none, so the animation itself turns off. Only the finished state stays on screen — a settled badge, a visible card, a filled track. Items 05 and 07 never attach the motion in the first place, since motion-safe: scopes the hover lift and the group-hover nudge to visitors who haven't asked for less movement. Item 06's panel keeps opacity-100 doing the work of showing "open" even with motion turned off, so the state itself is never lost, only the transition getting there. The same guard reads just as plainly written as a raw media query:

@media (prefers-reduced-motion: reduce) {
  .badge { animation: none; }
}

The same guard as a raw media query

MDN's prefers-reduced-motion reference lists the operating-system toggles this media feature reads from.

Where the rest lives

The rest of this site's CSS animation posts sit in the CSS category hub, and what's actually inside the download versus what's free to read here is covered on the about page.

FAQ

Do these tailwind animation examples work without the tw-animate-css plugin?

Eight of the nine need no plugin at all — @theme alone covers custom keyframes, state-based variants, and the built-in animate-spin extension. Only item 02's directional entrance classes come from tw-animate-css, and item 03 shows how to hand-roll that same kind of entrance with a single @theme variable instead.

Can I mix these Tailwind v4 animations with plain CSS or SCSS?

Yes — every class here compiles down to ordinary CSS, so nothing stops a hand-written @keyframes block from sitting next to a Tailwind utility in the same stylesheet. The archive ships an SCSS file per item that reproduces each animation without Tailwind at all, so the two approaches sit side by side for comparison.

Why does item 04 avoid the real animation-timeline: scroll() property?

Because browser support is still uneven — caniuse's scroll-timeline table shows Chrome and Edge shipping it at version 115, Safari following at 26 in September 2025, and Firefox still behind a flag as of September 2026. Shipping the arbitrary-property version today with no fallback would leave part of the audience looking at a track that never fills, so this demo swaps in a safe looping animation that reaches the same finished state. Driving motion from one piece of state is taken further, component by component, in CSS animations in React without a library.

Enter the archive password

The password is inside this article. You will find it as you read.