- CSS
CSS アニメーション|transitionとkeyframesの違いと実用20選
CSSアニメーションとは、CSSだけで要素の見た目を時間とともに変化させる仕組みで、「状態が変わったときに補間する transition」と「@keyframes で定義した動きを再生する animation」の2種類があります。
この記事では、どちらを使うかの判断表、全プロパティの一覧、実用パーツ20個のコピペ用コード をまとめました。すべて Playwright 1.56 + Chromium 141 で動かし、getAnimations() と getComputedStyle() で 途中の値まで実測 しました(数値で示します)。新機能の対応状況(2026年9月時点)、transform でレイアウト計算が1秒61回→1回に減った実測、prefers-reduced-motion の落とし穴も扱います。
CSSアニメーションの早見表
| やりたいこと | 仕組み | 書き方の核 | Chromium 141 実測 |
|---|---|---|---|
| ホバーで拡大・色変化 | transition | transition: transform .2s; |
100ms後 scale(1.0353) → 完了 1.05 |
| 表示時にフェードイン | animation | animation: fade-in .6s ease-out both; |
250ms後 opacity 0.654 |
| 回り続けるスピナー | animation | animation: spin 1s linear infinite; |
iterations Infinity |
| 点滅 | animation + steps() |
steps(1, end) infinite |
opacity は 1 と 0 のみ |
height: auto で開閉 |
transition | interpolate-size: allow-keywords; |
100ms後 48.39px → 84px |
display: none からフェード |
transition | @starting-style + allow-discrete |
100ms後 opacity 0.576 |
| モーダルの開閉 | transition | display / overlay に allow-discrete |
閉じて100ms後も display: block |
| スクロール量に連動 | animation | animation-timeline: scroll(root); |
scrollY 1000px で 26.178% |
| 画面に入ったら表示 | animation | animation-timeline: view(); |
画面外 0 → 画面内 1 |
| 画面切り替えをなめらかに | View Transitions | document.startViewTransition(fn) |
250ms のクロスフェードが自動で付く |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「CSS アニメーション|transitionとkeyframesの違いと実用20選」
transition と animation の違いと選び方
「ホバーやクラス付与で A→B を補間するなら transition、途中の段階・くり返し・自動再生が必要なら animation」 が判断の基本です。
| 比べる点 | transition | animation(@keyframes) |
|---|---|---|
| 動き出すきっかけ | 値の変化(:hover、クラス付与) |
適用された瞬間に自動再生 |
| 途中の段階 | 開始と終了の2点 | 0%〜100% の任意の点 |
| くり返し・一時停止 | できない | infinite / paused |
| 初回表示で動かす | @starting-style が必要 |
そのまま動く |
transition のプロパティ一覧
| プロパティ | 役割 | 例 | 初期値 |
|---|---|---|---|
transition-property |
補間するプロパティ | transform, opacity |
all |
transition-duration |
かける時間 | .3s |
0s |
transition-timing-function |
速度の変化 | ease-out |
ease |
transition-delay |
開始までの待ち時間 | .1s |
0s |
transition-behavior |
display など離散値も扱う |
allow-discrete |
normal |
/* プロパティ 時間 イージング 遅延(カンマで複数) */
.btn { transition: background-color .3s ease, transform .2s ease-out; }
.btn:hover { background-color: #1a73e8; transform: scale(1.05); }
- 時間の値が2つあれば1つ目が duration、2つ目が delay です。
- transition は通常の状態(
.btn)に書く と、ホバーを外したときも逆向きに動きます。
animation(@keyframes)のプロパティ一覧
| プロパティ | 役割 | よく使う値 | 初期値 |
|---|---|---|---|
animation-name |
@keyframes 名 | fade-in |
none |
animation-duration |
1回の時間 | .6s |
0s |
animation-timing-function |
速度の変化 | ease-out / linear / steps(4) |
ease |
animation-delay |
待ち時間(負の値で途中から) | .3s / -.5s |
0s |
animation-iteration-count |
回数 | 3 / infinite |
1 |
animation-direction |
向き | reverse / alternate |
normal |
animation-fill-mode |
前後の値を保つか | forwards / both |
none |
animation-play-state |
再生・停止 | paused |
running |
animation-timeline |
進める基準 | scroll() / view() |
auto |
@keyframes slide-up {
from { opacity: 0; transform: translateY(24px); }
to { opacity: 1; transform: none; }
}
/* 名前 時間 イージング 遅延 回数 向き fill-mode */
.item { animation: slide-up .6s ease-out .3s 1 normal both; }
animation-fill-mode の違い(実測)
省略(none)すると、終わった瞬間に元のスタイルへ戻ります。
| 指定 | 遅延中 | 終了後 | 実測 |
|---|---|---|---|
none |
元のスタイル | 元に戻る | 元の opacity 0.2 に戻った |
forwards |
元のスタイル | 最後の値を保持 | 1 のまま |
backwards |
最初の値 | 元に戻る | — |
both |
最初の値 | 最後の値を保持 | 遅延0.3sの要素が250ms時点で 0、1.25s時点で 1 |
イージング:cubic-bezier() と steps()
| 値 | 動き | 向いている用途 |
|---|---|---|
linear |
一定 | スピナー、スクロール連動 |
ease |
ゆっくり→速く→ゆっくり | 汎用 |
ease-in / ease-out |
ゆっくり始まる / ゆっくり終わる | 退場 / 登場・ホバー |
cubic-bezier(x1,y1,x2,y2) |
自由な曲線 | 行き過ぎて戻る動き |
steps(n, end) |
n段階でカクッと進む | 点滅、タイピング風 |
cubic-bezier() は y を 1 より大きくすると「少し行き過ぎて戻る」動き になります(例:cubic-bezier(.34, 1.56, .64, 1))。
steps() は途中を補間しません。 幅 0→12ch・2秒・steps(12, end) は250ms(1.5段階目)で幅 8px=1段階分でした。
実用パーツ20選(コピペ用)
Chromium 141 で動作を確認したコードです。
表示時に動かす(1〜5)
/* 1. フェードイン */
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
.fade-in { animation: fade-in .6s ease-out both; }
/* 2. 下からスライドイン(@keyframes slide-up は上の一覧のもの) */
.slide-up { animation: slide-up .6s ease-out both; }
/* 3. 順番に表示 */
.list > li { animation: slide-up .5s ease-out both; }
.list > li:nth-child(2) { animation-delay: .1s; }
.list > li:nth-child(3) { animation-delay: .2s; }
/* 4. タイピング風 */
@keyframes typing { from { width: 0; } }
.typing { overflow: hidden; white-space: nowrap; width: 12ch;
animation: typing 2s steps(12, end); }
/* 5. 追加した要素(通知)をフェードイン */
.toast { opacity: 1; transition: opacity .3s; }
@starting-style { .toast { opacity: 0; } }
5は追加直後 @starting-style の値から補間が始まり、100ms後 opacity 0.468 でした。
ホバー・押下(6〜10)
/* 6. ホバーで拡大+色 */
.btn { transition: background-color .3s, transform .2s; }
.btn:hover { background-color: #1a73e8; transform: scale(1.05); }
/* 7. 押した感触 */
.btn:active { transform: scale(.96); }
/* 8. カードを浮かせる(行き過ぎて戻る) */
.card { transition: transform .3s cubic-bezier(.34, 1.56, .64, 1), box-shadow .3s; }
.card:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgb(0 0 0 / .15); }
/* 9. 左から伸びる下線 */
.underline { position: relative; }
.underline::after { content: ""; position: absolute; left: 0; bottom: 0;
width: 100%; height: 2px; background: currentColor;
transform: scaleX(0); transform-origin: left; transition: transform .3s; }
.underline:hover::after { transform: scaleX(1); }
/* 10. 入力エラーで揺らす */
@keyframes shake {
0%, 100% { transform: translateX(0); }
20%, 60% { transform: translateX(-6px); }
40%, 80% { transform: translateX(6px); }
}
.shake { animation: shake .4s; }
10の再実行はクラスを外して void el.offsetWidth; のあと付け直します。
くり返す動き(11〜14)
/* 11. スピナー */
@keyframes spin { to { transform: rotate(360deg); } }
.spinner { width: 32px; height: 32px; border: 4px solid #ddd;
border-top-color: #333; border-radius: 50%;
animation: spin 1s linear infinite; }
/* 12. 点滅(中間の値を作らない) */
@keyframes blink { 50% { opacity: 0; } }
.blink { animation: blink 1s steps(1, end) infinite; }
/* 13. 鼓動 */
@keyframes pulse { 50% { transform: scale(1.08); } }
.pulse { animation: pulse 1.5s ease-in-out infinite; }
/* 14. 一時停止 */
.spinner.is-paused { animation-play-state: paused; }
14の実測:1.25秒後、running のスピナーは currentTime が 1283ms、paused は 0 のままでした。
開閉・モーダル(15〜18)
/* 15. display: none からフェードイン */
.panel { display: none; opacity: 0;
transition: opacity .3s, display .3s allow-discrete; }
.panel.is-open { display: block; opacity: 1; }
@starting-style { .panel.is-open { opacity: 0; } }
/* 16. アコーディオン(height: auto へ補間) */
:root { interpolate-size: allow-keywords; }
.acc { height: 0; overflow: hidden; transition: height .3s; }
.acc.is-open { height: auto; } /* または height: calc-size(auto, size) */
/* 17. <details> をなめらかに開く */
details::details-content { height: 0; overflow: clip;
transition: height .3s, content-visibility .3s allow-discrete; }
details[open]::details-content { height: auto; }
/* 18. モーダル(<dialog>) */
dialog { opacity: 0; transform: translateY(16px);
transition: opacity .3s, transform .3s,
display .3s allow-discrete, overlay .3s allow-discrete; }
dialog[open] { opacity: 1; transform: none; }
@starting-style { dialog[open] { opacity: 0; transform: translateY(16px); } }
dialog::backdrop { background: rgb(0 0 0 / .4); }
openBtn.addEventListener('click', () => dialog.showModal());
closeBtn.addEventListener('click', () => dialog.close());
- 17は開いて60ms後
58.94px、完了後106pxでした。 - 18は
displayとoverlayにallow-discreteを付けると、閉じる動きが終わるまで表示とトップレイヤーが保たれます。close()から100ms後もdisplay: block(opacity0.531)、500ms後にnoneでした。
スクロール・画面切り替え(19〜20)
/* 19-a. 読了プログレスバー */
@keyframes grow { from { transform: scaleX(0); } }
.progress { position: fixed; top: 0; left: 0; width: 100%; height: 4px;
background: #1a73e8; transform-origin: left;
animation: grow linear both;
animation-timeline: scroll(root); /* ショートハンドより後に書く */ }
/* 19-b. 画面に入ったら表示 */
@keyframes reveal { from { opacity: 0; transform: translateY(40px); } }
@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: view()) {
.reveal { animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% cover 30%; }
}
}
animation-timeline はショートハンドで初期値に戻るため、後に書きます。 スクロール位置で進むので duration は不要です。
/* 20. View Transitions(同じページ内の切り替え) */
const update = () => { title.textContent = '更新しました'; };
document.startViewTransition ? document.startViewTransition(update) : update();
/* 時間を変える/特定の要素を別に動かす */
::view-transition-old(root), ::view-transition-new(root) { animation-duration: .4s; }
.hero { view-transition-name: hero; }
/* ページをまたぐ遷移は両方のページに */
@view-transition { navigation: auto; }
Chromium 141 で動いているか確認した結果
環境:Playwright 1.56 + Chromium 141.0.7390.37(ヘッドレス、1280×800)。
読み込み直後の getAnimations()
| 要素 | 名前 | 状態 | 回数 | 時間/遅延(ms) | fill | タイムライン |
|---|---|---|---|---|---|---|
.fade-in |
fade-in | running | 1 | 600 / 0 | both | DocumentTimeline |
遅延つき .slide-up |
slide-up | running | 1 | 600 / 300 | both | DocumentTimeline |
.spinner |
spin | running | Infinity | 1000 / 0 | none | DocumentTimeline |
.is-paused |
spin | paused | Infinity | 1000 / 0 | none | DocumentTimeline |
.progress |
grow | running | 1 | (スクロール) | both | ScrollTimeline |
.reveal |
reveal | running | 1 | (スクロール) | both | ViewTimeline |
スクロール連動と View Transitions
| scrollY | プログレスバー scaleX | currentTime |
.reveal opacity |
|---|---|---|---|
| 0 | 0 |
0% |
0 |
| 1000 | 0.26178 |
26.178% |
0 |
| 2200 | 0.575916 |
57.5916% |
1 |
| 3820(最下部) | 1 |
100% |
1 |
スクロール連動の currentTime はパーセントで返りました。 startViewTransition() では ::view-transition-old(root) に -ua-view-transition-fade-out、new に fade-in などが 250ms で自動的に付きました。
よくあるつまずきと直し方(直す前と後の実測)
| つまずき | 直す前(実測) | 直し方 | 直した後(実測) |
|---|---|---|---|
height: auto の開閉が一瞬 |
100ms後すでに 84px、transition なし |
:root { interpolate-size: allow-keywords; } か calc-size(auto, size) |
100ms後 48.39px → 84px |
display: none から表示でフェードしない |
100ms後 opacity 1、transition なし |
display .3s allow-discrete + @starting-style |
100ms後 0.576 |
| 終わると元に戻る | 終了後 opacity 0.2 |
animation-fill-mode: forwards |
1 を保持 |
| スクロール連動が効かない | animation-timeline がショートハンドで上書き |
ショートハンドの 後 に書く | ScrollTimeline で進行 |
display: none の要素には変化前のスタイルがなく、@starting-style がその開始値を与えます。 どの直し方も、未対応ブラウザでは「一瞬で切り替わる」だけなので段階的に導入できます。
パフォーマンス:transform と opacity を選ぶ理由
ブラウザの描画は「スタイル計算 → レイアウト → ペイント → 合成」の順です。 left や width を動かすと毎フレームレイアウトからやり直しますが、transform と opacity は多くの場合、合成の段階だけで処理 できます。
Performance.getMetrics(DevTools Protocol)で1秒間の回数を比べました。
| 動かしたプロパティ | レイアウト回数/秒 | スタイル再計算/秒 |
|---|---|---|
left: 0 → 100px |
61回 | 61回 |
transform: translateX(0 → 100px) |
1回 | 1回 |
位置は translate()、大きさは scale()、表示は opacity で動かす と合成で済みます。
will-change: transform; は事前にレイヤーを用意させる指定 です。メモリを使うため、カクつきが確認できた要素だけに付けるのが基本です。
prefers-reduced-motion への対応
OSの「動きを減らす」設定は prefers-reduced-motion: reduce で検出できます。
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: .01ms !important;
animation-delay: 0s !important;
animation-iteration-count: 1 !important;
transition-duration: .01ms !important;
scroll-behavior: auto !important;
}
}
emulateMedia({ reducedMotion: 'reduce' }) で確認した結果です。
| 項目 | animation-delay 行なし |
行あり |
|---|---|---|
| スピナーの時間 / 回数 | 1e-05s / 1(即終了) |
同じ |
| 遅延0.3sの要素(50ms後) | running(まだ待機中) |
finished |
| スクロール連動(50ms後) | running |
running |
時間を縮めるだけでは遅延とスクロール連動が残ります。 遅延は animation-delay: 0s、スクロール連動は19-bのように no-preference の中でだけ付ける と確実です。
新しい機能の対応状況(2026年9月時点)
| 機能 | 状況 | 出典・補足 |
|---|---|---|
transition / animation |
主要ブラウザすべて | — |
@starting-style、allow-discrete |
Baseline(2024年8月〜) | MDN |
| View Transitions(同一ドキュメント) | Baseline(2025年10月〜) | Firefox 144 で3エンジン対応(web.dev) |
animation-timeline(scroll() / view()) |
一部のみ | Chrome 115以降・Safari 26。MDN は Limited availability |
interpolate-size / calc-size() |
Chromium系のみ | Chrome 129以降(web-features) |
Chromium 141 では CSS.supports() などで上の機能すべてが true でした。「最初は非表示」のパーツだけ @supports で分岐 すると、未対応ブラウザでも内容が見えます。
よくある質問
Q. transition と animation はどちらを使えばよいですか? A. ホバーやクラスの付け外しで A→B を切り替えるなら transition、くり返し・途中の段階・自動再生が必要なら animation です。「くり返すか」で判断すると決めやすくなります。
Q. アニメーションがカクつきます。
A. 動かすプロパティを transform と opacity に置き換え、それでも必要な要素だけに will-change を付けます。
まとめ
- transition は「状態変化の2点を補間」、animation は「@keyframes を自動再生・くり返し」
- 時間が2つなら「duration → delay」の順。
animation-timelineはショートハンドの後に書く height: autoはinterpolate-size: allow-keywords、display: noneからは@starting-style+allow-discrete- 動かすのは
transformとopacity。実測でレイアウトが61回→1回/秒に - スクロール連動と View Transitions はJSなしで使える。対応状況を見て段階的に
prefers-reduced-motionは delay とスクロール連動まで含めて対応 する
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「CSS アニメーション|transitionとkeyframesの違いと実用20選」
株式会社RJCでは、画面の心地よい動きとパフォーマンスの両立を、実際に計測しながらプロジェクトの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。