GODRICH

Tailwind アニメーション9選(v4) 一行でコピペOK

tailwind アニメーションは、tailwind.config.jsを使わずCSSの@themeブロックに--animate-変数と@keyframesを一緒に書くと、その名前がそのままユーティリティクラスになるTailwind v4のCSS-firstな作り方です。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

九つの並び順は人気順ではなく、Tailwind v4の構文を学ぶ順に揃えています。まず@themeでカスタムアニメーションを登録する方法(01〜03)、次にブラウザ対応が割れる機能を安全に再現する方法(04)、状態に反応させる書き方(05・06)、アクセシビリティのガード(07)、最後に既存ユーティリティを拡張する書き方(08・09)という流れです。コードはTailwindのクラス片だけを抜き出し、完全版とSCSS版は圧縮ファイル一つにまとめてあります。

01@themeカスタムキーフレーム

tailwind.config.jsなしにCSSの@themeブロック内へ--animate-pop-spin変数と@keyframesを一緒に書くと、その名前がそのままanimate-pop-spinユーティリティクラスになります。バッジが回転しながら飛び出す登場アニメーションや、通知カウントの更新に向いています。

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登場ユーティリティ

tw-animate-cssプラグインのanimate-in fade-in slide-in-from-top-8 duration-500をそのまま使うと、カードが上32pxから透明な状態で始まり、定位置へ降りながらはっきり見えてきます。モーダルやトーストの初回表示、カードリストの登場に向いています。

@keyframes slide-intranslateY(-32px)500ms
npm install -D tw-animate-css
<!-- 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>

03フェード+スライドユーティリティ

プラグインなしで@theme--animate-fade-slideを一つ定義し、opacityとtranslateYを一つのキーフレームにまとめると、段落が下16pxから浮かび上がる登場アニメーションがクラス一つで完結します。ヒーロー文やセクションタイトルの登場に向いています。

@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>

04任意値scroll-timeline

Tailwindにはscroll-timeline専用のユーティリティがなく、任意プロパティ[animation-timeline:scroll()]で付けますが、ブラウザごとに対応が割れるためこのデモは同じ進捗を安全なループで再現しています。長文記事の読了プログレスバーやLPのスクロール進捗インジケーターに向いています。

@keyframes fill-loop임의 속성caniuse 확인
<!-- 対応ブラウザでのみ有効。下の構文を使う前にcaniuse.comで対応範囲を確認してください -->
<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アイコン

親要素にgroupを付け、アイコンにgroup-hover:translate-x-1を付けると、カードのどこをホバーしても内側の矢印だけが右へ4px滑ります。カード型CTAやリスト項目の矢印に向いています。

.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">もっと見る</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アニメーション(shadcn)

shadcnのコンポーネントと同じく、JSはdata-state="open"だけを切り替え、CSSはdata-[state=open]:修飾子でscale-y-100opacity-100を与えます。widthやheight、grid-template-rowsでレイアウトを再計算させないのが要点です。

[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]">
  配送案内
</button>
<div id="p" data-state="closed"
     class="origin-top scale-y-0 opacity-0 transition-all duration-200
            data-[state=open]:scale-y-100 data-[state=open]:opacity-100">
  <p>決済後1〜2日以内に発送されます。</p>
</div>

07motion-safeガード

別途@mediaを書かなくてもmotion-safe:hover:-translate-y-1motion-reduce:hover:shadow-mdを並べて付けるだけで、動きを減らした人には影が薄くなるだけで浮き上がる動作自体が最初から付きません。保存ボタンのようなプライマリCTAのホバーフィードバックに向いています。

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-200
         motion-safe:hover:-translate-y-1 motion-safe:hover:shadow-lg
         motion-reduce:hover:shadow-md"
>
  変更を保存
</button>

08spinユーティリティ

既定のanimate-spin(1秒)はそのままに、@theme--animate-spin-slow: spin 3s linear infinite;を足すだけで、ロゴとローディングリングが同じ回転を別々の速度で一緒に回ります。新しい@keyframesを作り直す必要がありません。

@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>

09スタガーユーティリティ

リスト項目ごとに[animation-delay:calc(var(--i)*80ms)]を付け、--iを0から1つずつ増やすだけで、同じanimate-fade-slideクラス一つが80msずつ遅れて順番に浮かび上がります。通知リストの登場やダッシュボードカードの順次ロードに向いています。

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">
    新しいコメントが届きました
  </li>
  <li style="--i:1" class="animate-fade-slide [animation-delay:calc(var(--i)*80ms)] rounded-lg bg-white/8 px-4 py-2">
    フォロワーが増えました
  </li>
  <li style="--i:2" class="animate-fade-slide [animation-delay:calc(var(--i)*80ms)] rounded-lg bg-white/8 px-4 py-2">
    決済が完了しました
  </li>
</ul>

どこで崩れるか — 落とし穴

一番よく踏む落とし穴はv3からv4への移行時です。tailwind.config.jstheme.extend.keyframesanimationをそのまま残してクラス名だけanimate-pop-spinに変えても、何も起きません。Tailwind v4はCSS-firstで設定ファイルを読まず、公式ドキュメントが案内するとおり定義はCSSの@themeブロック内に置く必要があります。06のdata-stateアコーディオンを組むときにも似た落とし穴がありました。grid-template-rows: 0fr→1frで高さを開閉する方法はよく使われますが、これはブラウザが毎フレームでトラックサイズを再計算するレイアウトプロパティなので、この記事のtransform・opacity原則と衝突します。9項目はどれも同じ原則を守っているため、06のアコーディオンもscaleYtransform-origin: topで開閉する形にしました(高さの空間はあらかじめ確保される代わり、実際に使うときは親をoverflow:hiddenで囲むと安全です)。08のspinユーティリティでも近いことがありました。二つ目のリングに新しい@keyframesをまた作る代わりにanimation-durationだけを上書きしたところ、コンパイル後のCSSサイズは増えませんでした。同じspinキーフレームを速度だけ変えて共有するほうが、ユーティリティの数を増やさずに済みます。ここまでの修正を反映した9ファイルの圧縮パスワードはmtsjkxrfで、下のボタンから受け取れます。

アクセシビリティ

9項目ともprefers-reduced-motionに対応していますが、やり方は三通りに分かれます。01・02・03・04・06・08・09はmotion-reduce:animate-none系でアニメーションそのものを止めて最終状態だけを見せます。07はmotion-safe:修飾子の中にだけhover動作を書いて、そもそも動きを付けません。05はホバー時の移動そのものは残しつつtransitionだけを外し、アニメーションなしで瞬時に切り替わるようにしています。06のパネルは動きを止めても開閉状態がopacityで分かるように残しています。

<span class="animate-pop-spin motion-reduce:animate-none rounded-full bg-[#fff7e6] px-4 py-2 font-bold">NEW</span>
<button class="shadow-md transition-all duration-200 motion-safe:hover:-translate-y-1 motion-safe:hover:shadow-lg motion-reduce:hover:shadow-md">
  変更を保存
</button>

この対応範囲はMDNのprefers-reduced-motionで確認できます。

Tailwindを使わない素のCSSだけのアニメーションはCSSアニメーション一覧ページに集めてあります。ブログ全体の狙いはこのブログについてのページに書きました。

FAQ

v3のtailwind.config.jsにあったアニメーション設定はv4でもそのまま使えますか?

いいえ。v4は設定ファイルを読まないCSS-first構造なので、theme.extend.keyframestheme.extend.animationは無視されます。同じ内容をCSSの@themeブロック内へ--animate-*変数と@keyframesとして書き直す必要があります。

tw-animate-cssのようなプラグインなしでも登場アニメーションは作れますか?

作れます。03のように@themeへ独自の--animate-*変数を一つ定義するだけで、プラグインなしでも同じ効果が出せます。ただしslide-in-from-*のように方向違いの組み合わせが多い場合は、プラグインの方がコード量を減らせます。

Reactでdata-stateの切り替えはどう書けばいいですか?

zipのreact/フォルダではuseStateで開閉状態を持ち、それをそのままdata-state属性へ反映しています。SCSSモジュール側は[data-state="open"]セレクタだけを見て動くので、コンポーネント側は状態を一つ切り替えるだけで済みます。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。