GODRICH

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.

01@theme Custom Keyframes

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. It suits a badge that only needs to spin into view once, such as a new-item tag or an updated notification count.

cubic-bezier 오버슈트@keyframes 동봉900ms
<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

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. It fits a toast or a modal's very first appearance, or a card sliding into a list.

@keyframes slide-intranslateY(-32px)500ms
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

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. It works well for a hero line or a section title that should feel like it's settling into place.

@keyframes fade-slidetranslateY+opacity400ms
<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

Tailwind ships no dedicated scroll-timeline utility, so it gets attached through the arbitrary property [animation-timeline:scroll()] instead. Because browser support still varies, this demo reproduces the same fill progress with a safe looping animation rather than the real scroll-driven one.

@keyframes fill-loop임의 속성caniuse 확인
<!-- 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

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. It's a light nudge that suits a card-style call to action or an arrow sitting at the end of a list row.

.card:hover 자식translateX(4px)200ms
<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)

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. Nothing here touches width, height, or grid-template-rows, so the panel opens without forcing a layout recalculation.

[data-state=open]scaleY(1)attr 토글 JS
<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

Pairing motion-safe:hover:-translate-y-1 with motion-reduce:hover:shadow-md needs no separate @media block at all. A reduced-motion visitor only sees the shadow soften, because the lift itself is never attached in the first place.

prefers-reduced-motion: no-preferencereduce 는 box-shadow 만220ms
<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

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. Neither utility needs its own @keyframes block, since both point back to the same spin keyframe Tailwind already ships.

@keyframes spinrotate(360deg) 공유linear infinite
<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

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. It reads as a sequence instead of everything appearing on screen at once.

var(--i)animation-delay 계산80ms 간격
<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?

The most common failure happens on the way from v3 to v4. 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: animating 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

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

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

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 v4 animations 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.

Enter the archive password

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