CSSアニメーションが重い・酔うを直す:transform中心の実装とprefers-reduced-motion対応
@keyframesとtransitionの使い分け、transform/opacityでGPUに乗せる、will-changeの正しい使い方、prefers-reduced-motion対応、スクロール連動まで。動くコード付き。
「このカード、ふわっと出して」とClaude Codeに頼んだら、たしかにふわっと出ました。
でも実機のAndroidで開いたら、カードがカクカク震えながら降りてくる。スクロールも一緒に引っかかる。原因を追ったら、生成されたCSSが top と height をアニメーションさせていました。見た目の指示は通っていたのに、裏でブラウザが毎フレーム計算をやり直していたんです。
CSSアニメーションは「動けば正解」ではありません。同じ見た目でも、動かすプロパティ次第で60fpsにもなれば、ガタガタの15fpsにもなる。しかも、動きが苦手な人にとっては酔いの原因にもなります。
この記事では、僕が実機で何度もカクつかせては直してきた経験から、軽くて・酔わせず・壊れにくいCSSアニメーションの作り方をまとめます。難しい仕様の暗記ではなく、「どれを動かすか」「いつ止めるか」という判断のほうに重心を置きます。
この記事の要点
- アニメーションは
transformとopacityを中心に作る。この2つはGPUの合成だけで処理でき、レイアウト再計算(カクつきの主犯)を起こさない。 top/left/width/height/marginを動かすと周りの要素まで再計算が走る。これがガタつきの原因。transitionは「開始と終了だけ」の小さな変化用。@keyframesは「途中経過を設計したいとき」用。役割で分ける。will-changeは常時つけると逆効果。動く直前だけ予約し、終わったら外すのが正解。prefers-reduced-motionへの対応は健康配慮であり、やらないと公開事故になる。1ブロックで全体を抑えられる。
カクつきの正体は「レイアウトの再計算」
まず、なぜ動かすプロパティで差が出るのかだけ押さえます。ここが分かると、あとの判断が全部ラクになります。
ブラウザは画面を作るとき、ざっくり3工程を踏みます。
| 工程 | 何をするか | 重さ |
|---|---|---|
| レイアウト | 要素の大きさと位置を計算する | 重い(周りも巻き込む) |
| ペイント | 色・影・文字を描く | 中くらい |
| 合成(compositing) | 描いたものをレイヤーとして重ねる | 軽い(GPU担当) |
width や top を変えると、その要素の大きさや位置が変わるので、ブラウザは「じゃあ隣の要素はどこに動く?」と周りまで計算し直します。これがレイアウトの再計算で、英語だと layout thrash(レイアウトの暴れ)と呼ばれる状態です。1秒に60回これをやれば、当然カクつきます。
いっぽう transform: translateY(...) や opacity は、要素を「すでに描き終えた1枚の絵」として扱い、その絵をズラしたり透かしたりするだけです。周りの位置は計算し直しません。だから軽い。
結論はシンプルです。動かしたいのが「移動」なら transform: translate、「拡大縮小」なら transform: scale、「表示・非表示」なら opacity。この3つに寄せるだけで、体感の滑らかさが変わります。詳しい挙動はMDNのCSS transformが一次情報として正確です。
ページ全体の速度を測る話はClaude Codeでパフォーマンス最適化に書いたので、Core Web Vitalsまで踏み込みたい人はそちらへ。
transitionとkeyframes、どっちを使う?
両方「動かす」道具なので混同しがちですが、向き不向きがはっきりしています。
- transition:開始の値と終了の値だけ決めれば、間はブラウザが補完してくれる。hover で色が変わる、ボタンが少し浮く、開閉する——こういう「A から B へ」の単純な変化に向く。
- @keyframes:
0%60%100%のように途中の通過点まで指定できる。ローディングのループ、登場演出、注意を引く段階的な動きなど、タイムラインを自分で設計したいときに使う。
迷ったら「途中経過にこだわりがあるか?」で決めます。こだわりがなければ transition のほうが短く書けて壊れにくい。
下のコードは、ボタンの hover を transition、通知の登場を keyframes に分けた最小例です。値はデザイントークン(--motion-* という名前付きの設定値)にまとめてあるので、サイト全体で動きの速さを揃えられます。
:root {
/* アニメーションの設定値に名前をつけて一元管理する */
--motion-duration-fast: 160ms;
--motion-duration-normal: 280ms;
--motion-ease-standard: cubic-bezier(0.2, 0, 0, 1);
--motion-distance-sm: 12px;
}
/* 単純な状態変化は transition で十分 */
.button {
transition:
background-color var(--motion-duration-fast) var(--motion-ease-standard),
transform var(--motion-duration-fast) var(--motion-ease-standard);
}
.button:hover {
/* 移動は top ではなく transform で。レイアウト再計算が走らない */
transform: translateY(-2px);
}
/* 途中経過を設計したい登場演出は keyframes で */
.notice {
opacity: 0;
transform: translateY(var(--motion-distance-sm));
animation: notice-enter var(--motion-duration-normal)
var(--motion-ease-standard) forwards;
}
@keyframes notice-enter {
from {
opacity: 0;
transform: translateY(var(--motion-distance-sm));
}
to {
opacity: 1;
transform: translateY(0);
}
}
Claude Codeに依頼するときも、最初に「hover は transition、登場は keyframes」と役割を指定すると生成が安定します。「いい感じに動かして」だけだと、height をアニメーションさせるコードが平気で混ざってきます。トークンの設計そのものを深掘りしたいならClaude CodeでCSS変数を実務投入する設計も合わせてどうぞ。
will-changeは「保険」ではなく「予約」
ここ、いちばん誤解が多いところです。
will-change は「この要素、これから動くよ」とブラウザに前もって伝えるプロパティです。伝えておくと、ブラウザはその要素を独立したレイヤーに分けて準備しておくので、動き出しがカクつきにくくなります。
問題は、これを「お守り」みたいに全要素へ付けてしまうケース。will-change: transform を100個の要素に常時つけると、ブラウザは100枚のレイヤーをずっと抱え続けます。メモリを食い、かえって遅くなります。効くどころか逆効果です。
正しい使い方は「動く直前に予約して、終わったら外す」。
/* 常時はつけない。動く直前にだけ予約する */
.card {
/* ここに will-change は書かない */
transition: transform 200ms cubic-bezier(0.2, 0, 0, 1);
}
/* hover の意図が出た瞬間に予約 */
.card-zone:hover .card {
will-change: transform;
}
.card:hover {
transform: scale(1.03);
}
JavaScriptで重いアニメーションを始めるなら、開始直前に el.style.willChange = "transform"、終了イベント(transitionend など)で el.style.willChange = "auto" と外すのが定石です。「付けっぱなしにしない」とだけ覚えておけば事故りません。web.devのアニメーション性能ガイドでも、動かすプロパティの選択が最優先で、will-change は補助だと説明されています。
prefers-reduced-motionは「やらないと事故」
prefers-reduced-motion は、ユーザーがOSやブラウザで「動きを減らしてほしい」と設定しているかをCSS側から読み取る仕組みです。前庭障害でめまいが出る人、ADHDで動きに気が散る人、単純に派手な動きが苦手な人——けっこうな割合でいます。
ここを無視した派手なサイトは、その人たちにとって「開いた瞬間に気分が悪くなるページ」になります。これは好みの問題ではなく、アクセシビリティの公開事故です。
ありがたいことに、対応は1ブロックでほぼ片付きます。
/* 「動きを減らして」設定の人には、動きを最小化する */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
/* ループ系は完全に止めて、最終状態だけ見せる */
.scroll-progress {
animation: none;
transform: scaleX(1);
}
.skeleton {
animation: none;
background-image: none;
}
}
ポイントは、動きを消しても情報は残すこと。アニメーションでしか見えない要素(スクロールしないと出てこないテキストなど)は、reduced motion で消えると内容ごと消えます。だから「動かない状態でも全部読める」を初めから作っておきます。仕様と例はMDNのprefers-reduced-motionが確実です。配慮全般はClaude Codeでアクセシビリティ対応を実装するにまとめてあります。
スクロール連動は「無くても読める」を死守する
最近のCSSには、JavaScript無しでスクロール量に動きを連動させる scroll-driven animations(スクロール連動アニメーション)があります。animation-timeline を使うやつです。進捗バーや、スクロールで要素がフッと現れる演出が、数行で書けます。
2026年6月時点の対応状況だけ正直に書いておきます。Chrome / Edge は115以降で対応、Safariは26で対応が入りました。Firefoxは実装済みですが、まだ about:config のフラグの裏(既定オフ)です。caniuseの集計でだいたい85%前後。つまり「使えるけど、全員には届かない」段階です。
だから鉄則は1つ。@supports で囲って、対応していないブラウザでは最初から見える状態にしておくこと。動かないことはあっても、読めないことは起きないようにします。
/* 上部のスクロール進捗バー */
.scroll-progress {
position: fixed;
inset: 0 auto auto 0;
z-index: 20;
width: 100%;
height: 3px;
transform-origin: left;
transform: scaleX(0);
background: var(--color-accent, #2563eb);
}
/* 対応ブラウザだけスクロールに連動させる */
@supports (animation-timeline: scroll()) {
.scroll-progress {
animation: progress-grow linear both;
animation-timeline: scroll();
}
}
@keyframes progress-grow {
to {
transform: scaleX(1);
}
}
/* 要素の登場演出:未対応では「最初から見えている」 */
.reveal {
opacity: 1;
transform: none;
}
@supports (animation-timeline: view()) {
.reveal {
opacity: 0;
transform: translateY(16px);
animation: reveal-up 1ms linear both;
animation-timeline: view();
animation-range: entry 10% cover 30%;
}
}
@keyframes reveal-up {
to {
opacity: 1;
transform: translateY(0);
}
}
.reveal の素の状態を opacity: 1 にしてある点が肝です。未対応ブラウザでは演出が走らないだけで、中身は最初から表示されます。逆に素を opacity: 0 にすると、未対応環境では「永遠に消えたまま」になります。これ、実際にやらかしました。最新仕様はMDNのScroll-driven animationsが追従しています。
効く場面と、あえて止める場面
動かす技術より、「どこで動かすか」の判断のほうが品質を分けます。僕の基準はこうです。
動かして効く場面
- 登場演出(記事カード、料金表)。同時に全部出すより主要部だけ短く出すと視線が誘導できる。ただしカードごとの遅延は合計80msを上限に。それ以上は「待たされている」だけになる。
- スケルトン表示。読み込み中の形を先に見せると、空白より不安が減る。ただし強い shimmer を延々ループさせると疲れるので、数秒超の処理には進捗テキストやキャンセル導線を足す。実装の型はスケルトンスクリーンの実装にある。
- 状態変化のフィードバック(保存完了、フィルタ適用)。短い動きで「何が変わったか」を伝えると認知負荷が下がる。
あえて止める場面
逆に、入力フォームのエラー文、決済確認、法的な注意書き、長い本文、管理画面の表、何度も開くメニュー——ここは動かさないほうが上品です。エラーをいちいち跳ねさせると、ユーザーは内容より動きに気を取られます。売上数値が毎回ピョコピョコ動くダッシュボードは、見るたびに作業効率を落とします。「意味のある変化」にだけ動きを割り当てると、プロダクト全体が落ち着きます。
僕がカクつかせて学んだ失敗3つ
正直に書きます。最初に作ったアニメーションは事故だらけでした。
ひとつ目は、冒頭の top と height でアニメーションさせたやつ。デスクトップのChromeでは滑らかに見えたので気づけませんでした。実機のミドルレンジAndroidで開いて初めてガタつきが分かった。それ以来、動きの確認は必ず非力な実機かCPUスロットリングをかけたDevToolsでやるようにしています。
ふたつ目は、will-change を全カードに付けっぱなしにしたこと。「速くなるおまじない」だと思っていました。実際はメモリを食ってスクロールがもっさりした。プロファイラでレイヤーが何十枚も増えているのを見て、ようやく外しました。
みっつ目は、reduced motion を後回しにして登場演出を opacity: 0 始まりで作ったこと。動きを止める設定の同僚の画面で、本文がまるごと消えていました。演出は飾りでも、中身は飾りじゃない。素の状態は必ず「見える」にする、と体で覚えました。
コピペで動く:最小デモHTML
ここまでの要点(transform中心 / will-changeは予約だけ / reduced motion対応)を1ファイルにまとめました。.html で保存してブラウザで開けば、そのまま動きます。OSの「視差効果を減らす」をオンにすると、登場演出が止まることも確認できます。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>軽いCSSアニメーションの最小デモ</title>
<style>
:root {
--dur: 260ms;
--ease: cubic-bezier(0.2, 0, 0, 1);
}
body {
font-family: system-ui, sans-serif;
margin: 0;
padding: 48px 16px;
background: #0f172a;
color: #e2e8f0;
}
.card-zone {
max-width: 360px;
margin: 0 auto;
}
.card {
background: #1e293b;
border-radius: 12px;
padding: 24px;
/* 登場演出:transform と opacity だけで動かす */
opacity: 0;
transform: translateY(16px);
animation: enter var(--dur) var(--ease) forwards;
/* hover 用に transition も用意 */
transition: transform 200ms var(--ease);
}
/* hover の意図が出た瞬間だけ will-change を予約する */
.card-zone:hover .card {
will-change: transform;
}
.card-zone:hover .card {
transform: scale(1.03);
}
@keyframes enter {
to {
opacity: 1;
transform: translateY(0);
}
}
/* 「動きを減らして」設定の人には演出を止め、中身は残す */
@media (prefers-reduced-motion: reduce) {
.card {
animation-duration: 0.01ms;
opacity: 1;
transform: none;
transition: none;
}
}
</style>
</head>
<body>
<div class="card-zone">
<div class="card">
<h2>ふわっと登場するカード</h2>
<p>transform と opacity だけで動かしているので、再描画が軽い。</p>
</div>
</div>
</body>
</html>
このデモは、移動を transform、表示を opacity に寄せ、will-change は hover の直前だけ予約し、reduced motion では演出を止めて中身を残す——記事の主張をそのまま体現しています。
よくある質問
Q. transform: translate と top / left、見た目が同じならどっちでもいい?
A. 見た目が同じでも中身は別物です。top / left はレイアウト再計算を起こし、transform はそれを避けてGPUの合成だけで動きます。アニメーションでの移動は迷わず transform を使ってください。
Q. will-change はとりあえず付けておけば速くなる?
A. 逆です。常時つけるとブラウザがレイヤーを抱え続け、メモリを食って遅くなります。動く直前に予約し、終わったら auto に戻すのが正しい使い方です。
Q. 高さ(アコーディオンの開閉)はどう動かせばいい?
A. height のアニメーションは再計算が走ります。要素の高さを grid-template-rows: 0fr → 1fr で動かす手法か、transform: scaleY での近似、または対応環境なら interpolate-size を検討してください。どうしても height を使うなら、頻繁に繰り返さないUIに限定します。
Q. スクロール連動アニメーションはもう本番で使える?
A. Chrome / Edge / Safari 26 では使えますが、Firefoxはフラグの裏で既定オフ、全体で85%前後です。必ず @supports で囲み、未対応でも内容が見える進化的強化(プログレッシブエンハンスメント)として入れてください。
Q. アニメーションの動作確認、何で見ればいい? A. デスクトップのChromeだけだとカクつきを見逃します。非力な実機か、DevToolsのCPUスロットリング(4x/6x slowdown)をかけて確認するのが確実です。横スクロールの発生やreduced motion時の表示は、Playwrightで自動チェックにも回せます。
実際に試した結果
transform と opacity に限定した登場演出は、実機のミドルレンジAndroidでもカクつかず、横スクロールも起きませんでした。冒頭の top / height 版とは体感がまるで違います。reduced motion をオンにしたPlaywrightで開いても、本文は表示されたまま残りました。
いっぽうで、スケルトンの shimmer は強くするほど「読み込みが長い」と感じさせたので、業務画面では静的なプレースホルダーに寄せるのが現実的でした。動かす技術より、動かさない判断のほうが効く場面は多い、というのが正直な実感です。まずは手元のカード一覧かCTAボタンを1つ選んで、上のデモHTMLのトークンと reduced motion ブロックを移植してみてください。
依頼文をチーム共通の型として残したい場合は、トレーニングと相談から実装レビューの流れをまとめています。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Codeのチーム利用でコストが読めない時に作る予算ログ
チーム導入前に、誰が何に使い、どの成果が出たかを見える化する予算ログの作り方。
コミット前の3分チェック: Claude Codeが触った範囲を確認してから確定する
Claude Codeが勝手に広げた変更を、コミット前に3分で見抜く確認手順。差分の範囲、検証ログ、ステージするファイルの絞り込みを順番に解説します。
Claude Codeをチーム導入する前に作る「リスク台帳」の中身
Claude Codeを個人実験で終わらせずチーム導入するための、権限・CI・公開の事故を防ぐリスク台帳の作り方を実例とコードで解説します。