GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

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版は圧縮ファイル一つにまとめてあります。クラス構文を使わない素のCSSで同じ結果へ辿り着く道は背景アニメーションで先に通っており、文章の一行だけを目立たせたい場合は@theme構文の次の一段としてテキスト強調アニメーションがあります。

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

@themeに変数とキーフレームを書く

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

@themeカスタムキーフレーム
新しいタブで開く

01 · @themeカスタムキーフレーム — 計測 累積面積 2.196% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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から透明な状態で始まり、定位置へ降りながらはっきり見えてきます。モーダルやトーストの初回表示、カードリストの登場に向いています。

tw-animate-css登場ユーティリティ
新しいタブで開く

02 · tw-animate-css登場ユーティリティ — 計測 累積面積 27.637% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

@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フェード+スライドユーティリティ

opacityとtranslateYを一つにまとめる

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

フェード+スライドユーティリティ
新しいタブで開く

03 · フェード+スライドユーティリティ — 計測 累積面積 6.125% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

@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 スクロール アニメーション」に専用ユーティリティはない

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

任意値scroll-timeline
新しいタブで開く

04 · 任意値scroll-timeline — 計測 累積面積 1.661% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

@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

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

group-hoverアイコン
新しいタブで開く

05 · group-hoverアイコン — 計測 累積面積 0.098% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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

カード型CTAの矢印に

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

JSはdata-stateだけを切り替える

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

data-stateアニメーション(shadcn)
新しいタブで開く

06 · data-stateアニメーション(shadcn) — 計測 累積面積 14.289% · 動いたフレーム 13/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

[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ガード

motion-safeとmotion-reduceを並べる

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

motion-safeガード
新しいタブで開く

07 · motion-safeガード — 計測 累積面積 3.297% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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

プライマリCTAのホバーに

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

既定のspinを残したまま足す

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

spinユーティリティ
新しいタブで開く

08 · spinユーティリティ — 計測 累積面積 1.432% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

@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スタガーユーティリティ

--iを1つずつ増やすだけ

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

スタガーユーティリティ
新しいタブで開く

09 · スタガーユーティリティ — 計測 累積面積 20.843% · 動いたフレーム 12/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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>

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

v4は設定ファイルを読まない

レイアウト属性ではなくtransformで開閉する判断は、重なるレイヤーが増えるほど効き目が大きくなります。紙を何枚も切って貼り重ねるペーパーカットのLPキットでは、同じ判断がセクション全体の滑らかさを分けます。

一番よく踏む落とし穴はv3からv4への移行時です。tailwind.config.jsのtheme.extend.keyframesとanimationをそのまま残してクラス名だけanimate-pop-spinに変えても、何も起きません。Tailwind v4はCSS-firstで設定ファイルを読まず、公式ドキュメントが案内するとおり定義はCSSの@themeブロック内に置く必要があります。06のdata-stateアコーディオンを組むときにも似た落とし穴がありました。grid-template-rows: 0fr→1frで高さを開閉する方法はよく使われますが、これはブラウザが毎フレームでトラックサイズを再計算するレイアウトプロパティなので、この記事のtransform・opacity原則と衝突します。9項目はどれも同じ原則を守っているため、06のアコーディオンもscaleYとtransform-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.keyframesとtheme.extend.animationは無視されます。同じ内容をCSSの@themeブロック内へ--animate-*変数と@keyframesとして書き直す必要があります。

「tailwind css アニメーション」はプラグインなしでも登場アニメーションを作れますか?

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

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

zipのreact/フォルダではuseStateで開閉状態を持ち、それをそのままdata-state属性へ反映しています。SCSSモジュール側は[data-state="open"]セレクタだけを見て動くので、コンポーネント側は状態を一つ切り替えるだけで済みます。状態ひとつで動きを支配するこの書き方をコンポーネント単位でさらに広げたのがライブラリなしのReact CSSアニメーションです。

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

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