CSSのtransitionやanimationを使ってアニメーションを実装したのに、なぜか動かないという経験はありませんか? コードは正しいはずなのに、要素がピクッとも動かない。ブラウザの開発者ツールを見ても原因がわからない。
実は、CSSアニメーションが効かない原因はパターンが決まっています。display: noneからの復帰、height: autoへの遷移、同一フレーム内の変更、@keyframesの定義ミス、prefers-reduced-motionの影響など、ハマりやすいポイントは限られています。
この記事では、transition・animationが効かないすべての原因パターンと、それぞれの具体的な解決方法を網羅的に解説します。問題のあるコードと修正後のコードを並べて示すので、自分のコードと照らし合わせてすぐに原因を特定できます。
この記事で学べること
- transition と animation の基本的な仕組みと違い
- transition が効かない4大原因(アニメーション不可プロパティ・初期値未指定・同一フレーム問題・display: none)
- animation が効かない3大原因(@keyframes定義ミス・プロパティ設定ミス・prefers-reduced-motion)
- CSS詳細度・上書き問題の診断と解決
- ブラウザ互換性とベンダープレフィックスの対応
- 実装パターン集(フェードイン・スライドイン・アコーディオン・ローディングスピナー等)
- Chrome DevTools を使ったアニメーションのデバッグ方法
- パフォーマンスのベストプラクティス(GPU対応プロパティ)
transition / animation の基本おさらい
まずはtransitionとanimationの基本を確認しましょう。「効かない原因」を理解するためには、そもそもどういう仕組みで動いているのかを知ることが重要です。
transition の仕組みと構文
transitionは、CSSプロパティの値が変化したときに、変化前の値から変化後の値へスムーズに遷移させる機能です。ホバーやクラスの付け替えなど、何らかのきっかけでプロパティの値が変わったときに自動的にアニメーションが発生します。
CSS – transition の基本構文
/* 個別指定 */
transition-property: opacity;
transition-duration: 0.3s;
transition-timing-function: ease;
transition-delay: 0s;
/* ショートハンド */
transition: opacity 0.3s ease 0s;
/* 複数プロパティ */
transition: opacity 0.3s ease,
transform 0.3s ease;
/* すべてのプロパティ */
transition: all 0.3s ease;
| プロパティ |
説明 |
デフォルト値 |
transition-property |
アニメーション対象のプロパティ |
all |
transition-duration |
アニメーションの所要時間 |
0s |
transition-timing-function |
アニメーションの速度曲線 |
ease |
transition-delay |
アニメーション開始までの遅延 |
0s |
transitionの重要なポイントは、変化のきっかけ(トリガー)が必要ということです。:hover、:focus、:activeなどの疑似クラスや、JavaScriptによるクラスの付け替え・スタイルの直接変更がトリガーとなります。
CSS – transition の使用例
.button {
background-color: #3b82f6;
color: #fff;
padding: 10px 20px;
border-radius: 8px;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #2563eb;
}
animation / @keyframes の仕組みと構文
animationは、@keyframesルールで定義したキーフレーム(中間状態)を順番に再生する機能です。transitionが「AからBへの遷移」しかできないのに対し、animationは「A → B → C → D」のように複数の中間状態を定義でき、トリガーなしで自動再生することもできます。
CSS – animation / @keyframes の基本構文
/* @keyframes でアニメーションを定義 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 要素にアニメーションを適用 */
.element {
animation: fadeIn 0.5s ease forwards;
}
CSS – @keyframes パーセント指定
@keyframes bounce {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-30px);
}
100% {
transform: translateY(0);
}
}
| プロパティ |
説明 |
デフォルト値 |
animation-name |
@keyframes の名前 |
none |
animation-duration |
1回のアニメーション時間 |
0s |
animation-timing-function |
速度曲線 |
ease |
animation-delay |
開始までの遅延 |
0s |
animation-iteration-count |
繰り返し回数 |
1 |
animation-direction |
再生方向 |
normal |
animation-fill-mode |
アニメーション前後のスタイル |
none |
animation-play-state |
再生/一時停止 |
running |
transition と animation の違い比較表
両者は似ているようで、役割と動作が大きく異なります。使い分けのポイントを押さえましょう。
| 比較項目 |
transition |
animation |
| トリガー |
必要(:hover、クラス変更等) |
不要(自動再生可能) |
| 中間状態 |
AからBへの2点間のみ |
複数の中間状態を定義可能 |
| 繰り返し |
不可(トリガーが必要) |
無限ループ可能 |
| 逆再生 |
自動(トリガー解除で戻る) |
animation-directionで指定 |
| 定義方法 |
プロパティのみ |
@keyframes + animationプロパティ |
| 用途 |
ホバー効果、状態変化 |
ローディング、注目演出 |
| JS制御 |
クラス切替が一般的 |
play-stateやクラスで制御 |
ポイント:シンプルな状態変化(ホバー、表示/非表示)にはtransition、複雑な動きや自動再生にはanimationを使うのが基本です。
transition が効かない原因①:アニメーション不可能なプロパティ
transitionが効かない最もよくある原因が、アニメーション不可能なプロパティを対象にしているケースです。すべてのCSSプロパティがtransitionできるわけではありません。
transition できるプロパティ・できないプロパティ
transitionが動作するには、プロパティの値が数値的に補間(interpolate)可能である必要があります。色やサイズのように中間値を計算できるプロパティは transition でき、displayのように離散的に切り替わるプロパティは transition できません。
| transition できるプロパティ(代表例) |
| サイズ系 |
width, height, max-width, max-height, min-width, min-height, padding, margin |
| 位置系 |
top, right, bottom, left |
| 色系 |
color, background-color, border-color, outline-color |
| 透明度 |
opacity |
| 変形 |
transform(translate, rotate, scale, skew) |
| 影 |
box-shadow, text-shadow |
| フィルター |
filter, backdrop-filter |
| 角丸 |
border-radius |
| テキスト |
font-size, letter-spacing, word-spacing, line-height |
| transition できないプロパティ(代表例) |
display |
none / block / flex などの切り替えは補間不可能 |
position |
static / relative / absolute / fixed の切り替え |
float |
none / left / right の切り替え |
overflow |
visible / hidden / scroll / auto の切り替え |
content |
疑似要素の content プロパティ |
background-image |
画像の切り替え(グラデーション含む※) |
注意:background-imageは基本的に transition できませんが、background-positionやbackground-sizeは transition 可能です。グラデーションの色を変えたい場合は、background-colorや擬似要素のopacityを使う手法があります。
display: none → block は transition できない
最もよくある失敗パターンです。displayプロパティはアニメーション不可能なので、display: noneからdisplay: blockに変えても transition は一切効きません。
CSS – NG:display で transition しようとする
/* これは動かない */
.modal {
display: none;
opacity: 0;
transition: opacity 0.3s ease;
}
.modal.active {
display: block;
opacity: 1;
}
display: noneの要素はレンダリングツリーから完全に除外されます。そのため、display: blockに変えた瞬間に要素がレンダリングツリーに追加され、opacityの変化は同一フレームで処理されるため、transition が発火しません。
CSS – OK:visibility + opacity で代替
/* visibility + opacity パターン */
.modal {
visibility: hidden;
opacity: 0;
transition: opacity 0.3s ease,
visibility 0.3s ease;
}
.modal.active {
visibility: visible;
opacity: 1;
}
height: auto への transition ができない
height: 0からheight: autoへの transition もよくある失敗です。autoは数値ではないため、ブラウザが中間値を計算できません。
CSS – NG:height: auto への transition
/* これは動かない */
.accordion-body {
height: 0;
overflow: hidden;
transition: height 0.3s ease;
}
.accordion-body.open {
height: auto; /* auto は transition できない */
}
この問題には複数の解決策があります。
CSS – 解決策1:max-height を使う
.accordion-body {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.accordion-body.open {
max-height: 500px; /* コンテンツより大きい値を指定 */
}
注意:max-heightを使う方法は手軽ですが、実際のコンテンツ高さとmax-heightの値に差がある場合、アニメーションの速度が不自然になります。精密な制御が必要な場合はJavaScriptでscrollHeightを取得して設定しましょう。
JavaScript – 解決策2:scrollHeight を使う
const toggle = (element) => {
if (element.classList.contains('open')) {
// 閉じる
element.style.height = element.scrollHeight + 'px';
requestAnimationFrame(() => {
element.style.height = '0px';
});
element.classList.remove('open');
} else {
// 開く
element.style.height = element.scrollHeight + 'px';
element.classList.add('open');
element.addEventListener('transitionend', () => {
element.style.height = 'auto';
}, { once: true });
}
};
CSS – 解決策3:CSS Grid を使う(モダンな方法)
.accordion-body {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s ease;
}
.accordion-body.open {
grid-template-rows: 1fr;
}
.accordion-body > div {
overflow: hidden;
}
ポイント:CSS Grid のgrid-template-rows: 0fr → 1frを使う方法は、JavaScriptなしでheight: auto相当のアニメーションを実現できるモダンな手法です。Chrome 111+、Firefox 118+、Safari 17+ で対応しています。
visibility vs display の使い分け
display: noneとvisibility: hiddenは似ていますが、アニメーションの観点で重要な違いがあります。
| 特性 |
display: none |
visibility: hidden |
| レイアウト |
空間を占めない |
空間を占める |
| transition |
不可能 |
可能(遅延切り替え) |
| イベント |
受け取れない |
受け取れない |
| 子要素 |
すべて非表示 |
子で visible に戻せる |
| アクセシビリティ |
読み上げされない |
読み上げされない |
アニメーション付きの表示/非表示を実装する場合、visibility + opacity + pointer-eventsの組み合わせが最も実用的です。
CSS – 完全なフェード表示/非表示パターン
.overlay {
visibility: hidden;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease,
visibility 0.3s ease;
}
.overlay.active {
visibility: visible;
opacity: 1;
pointer-events: auto;
}
transition が効かない原因②:初期値の未指定
transitionは変化前の値と変化後の値の両方が明確に定義されていないと動作しません。初期値が暗黙的にautoや未定義の場合、ブラウザは中間値を計算できません。
変化前の値が明示されていない
最もよくあるのは、widthやheightに初期値を設定していないケースです。
CSS – NG:初期値が未指定
/* width の初期値が auto(暗黙的)なので transition できない */
.box {
/* width は指定なし → auto */
transition: width 0.3s ease;
}
.box:hover {
width: 300px;
}
CSS – OK:初期値を明示的に指定
.box {
width: 200px; /* 初期値を明示 */
transition: width 0.3s ease;
}
.box:hover {
width: 300px;
}
auto からの遷移ができない
autoはCSSにおいて「ブラウザが自動計算する値」を意味します。autoは数値ではないため、transitionの中間値を計算できません。以下のプロパティでautoが初期値になっている場合、注意が必要です。
| プロパティ |
auto の意味 |
解決策 |
width |
コンテンツに合わせて自動調整 |
具体的なpx/%値を初期値に指定 |
height |
コンテンツに合わせて自動調整 |
max-heightやgrid-template-rowsで代替 |
top / left |
配置を自動計算 |
初期値に0を明示的に指定 |
margin |
自動マージン(centering等) |
具体的な値を指定するか、transformで代替 |
0 から始める必要がある場合
数値型のプロパティでも、初期値が設定されていない場合に問題が発生することがあります。特にtransformを使う場合、初期状態が暗黙的にnoneの場合とtranslateX(0)の場合で挙動が異なることがあります。
CSS – transform の初期値の注意点
/* NG: transform の初期値が none の場合、関数が異なると補間が不自然になる可能性 */
.card {
/* transform: none(デフォルト) */
transition: transform 0.3s ease;
}
.card:hover {
transform: scale(1.05) rotate(2deg);
}
/* OK: 初期値を明示的に指定 */
.card {
transform: scale(1) rotate(0deg);
transition: transform 0.3s ease;
}
.card:hover {
transform: scale(1.05) rotate(2deg);
}
ポイント:transformのショートハンドで複数の関数を使う場合、hover前後で同じ関数を同じ順序で指定しましょう。noneからscale(1.05) rotate(2deg)への transition は正常に動作しますが、関数の数や順序が異なると予期しない補間が起きることがあります。
transition が効かない原因③:同一フレーム内の変更
JavaScriptで要素のスタイルを変更する際、同一フレーム内で初期値と最終値を設定すると transition が効かないという問題があります。これはブラウザの描画の仕組みに起因する問題です。
JSでclass追加直後にtransitionが効かない
要素を動的に生成してDOMに追加し、すぐにクラスを付けてアニメーションさせようとすると、transition が無視されます。
JavaScript – NG:同一フレーム内で変更
// これは動かない
const el = document.createElement('div');
el.className = 'notification';
document.body.appendChild(el);
// 同一フレーム内なので transition が効かない
el.classList.add('show');
この問題が発生する理由は、ブラウザがスタイルの計算をバッチ処理するためです。DOMに追加してすぐにクラスを変更すると、ブラウザは「初期状態」をレンダリングする前に「最終状態」のスタイルを適用してしまいます。結果として、変化前と変化後の差分が検出されず、transition が発火しません。
requestAnimationFrame での回避
requestAnimationFrameを使って、次の描画フレームまでクラスの追加を遅延させます。
JavaScript – OK:requestAnimationFrame で遅延
const el = document.createElement('div');
el.className = 'notification';
document.body.appendChild(el);
// 次のフレームまで待ってからクラスを追加
requestAnimationFrame(() => {
requestAnimationFrame(() => {
el.classList.add('show');
});
});
注意:requestAnimationFrameを1回だけ呼ぶと、ブラウザによっては同一フレーム内で実行される場合があります。確実にフレームを分けるには、二重のrequestAnimationFrame(ダブルRAF)を使うのが安全です。
reflow の強制トリガー
もう一つの方法として、reflow(再レイアウト)を強制的にトリガーする方法があります。これにより、ブラウザが初期状態のスタイルを確定し、その後の変更を transition として検出できます。
JavaScript – OK:reflow を強制トリガー
const el = document.createElement('div');
el.className = 'notification';
document.body.appendChild(el);
// reflow を強制トリガー
el.offsetHeight; // このプロパティを読むと reflow が発生する
// reflow 後なので transition が効く
el.classList.add('show');
reflow を強制するプロパティ/メソッドには以下のようなものがあります。
| reflow トリガー |
説明 |
element.offsetHeight |
要素の高さを取得(reflow発生) |
element.offsetWidth |
要素の幅を取得(reflow発生) |
element.getBoundingClientRect() |
要素の位置・サイズを取得(reflow発生) |
getComputedStyle(element) |
計算済みスタイルを取得(reflow発生) |
getComputedStyle での回避
getComputedStyleを使ってブラウザにスタイルの計算を強制し、初期状態を確定させる方法もあります。
JavaScript – OK:getComputedStyle で回避
const el = document.createElement('div');
el.className = 'notification';
document.body.appendChild(el);
// getComputedStyle でスタイル計算を強制
getComputedStyle(el).opacity;
// これで transition が正しく動作する
el.classList.add('show');
ポイント:3つの方法(ダブルRAF、offsetHeight、getComputedStyle)のうち、reflow強制(offsetHeight)が最もシンプルで確実です。ただし、パフォーマンスが重要な場面では、不必要なreflowを避けるためにrequestAnimationFrameを使いましょう。
transition が効かない原因④:display: none からの復帰
display: noneは要素をレンダリングツリーから完全に削除します。そのため、display: noneから他の値に戻した瞬間にすべてのプロパティが一気に適用され、transition が発火しません。これはCSSアニメーションにおける最大の落とし穴の一つです。
display: none → block で transition が無視される理由
ブラウザの描画プロセスを理解すると、この問題の本質が見えてきます。
display: none で transition が無視されるメカニズム
- Step 1: 要素に
display: none が適用されている → レンダリングツリーに存在しない
- Step 2:
display: block に変更 → 要素がレンダリングツリーに追加される
- Step 3: 同時に
opacity: 1 などの最終値も適用される
- Step 4: ブラウザは「変化前の状態」を知らないため、transition を計算できない
- 結果: アニメーションなしで最終状態がいきなり表示される
@starting-style(CSS新機能)での解決
CSS の新機能である@starting-styleを使うと、display: noneから表示されるときの「開始スタイル」を定義できます。これにより、displayの切り替えでも transition が動作するようになります。
CSS – @starting-style を使った解決策
.dialog {
display: none;
opacity: 0;
transform: scale(0.9);
transition: opacity 0.3s ease,
transform 0.3s ease,
display 0.3s ease allow-discrete;
}
.dialog.open {
display: block;
opacity: 1;
transform: scale(1);
}
/* display: none から表示されるときの開始スタイル */
@starting-style {
.dialog.open {
opacity: 0;
transform: scale(0.9);
}
}
注意:@starting-styleとtransition: display allow-discreteは2024年に主要ブラウザでサポートされた比較的新しい機能です。Chrome 117+、Edge 117+、Safari 17.4+、Firefox 129+ で対応しています。古いブラウザもサポートする場合は、次のvisibility + opacityパターンを使いましょう。
visibility + opacity パターン
display: noneの代わりにvisibility: hiddenとopacity: 0を組み合わせる方法は、ブラウザの互換性が高く、最も広く使われている手法です。
CSS – visibility + opacity パターン(完全版)
/* --- モーダルのオーバーレイ --- */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
/* 非表示状態 */
visibility: hidden;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease,
visibility 0.3s ease;
}
/* --- 表示状態 --- */
.modal-overlay.active {
visibility: visible;
opacity: 1;
pointer-events: auto;
}
/* --- モーダル本体 --- */
.modal-content {
transform: translateY(-20px);
transition: transform 0.3s ease;
}
.modal-overlay.active .modal-content {
transform: translateY(0);
}
max-height パターン(アコーディオン)
アコーディオンメニューのように、コンテンツの高さに応じて要素を開閉したい場合は、max-heightを使ったパターンが定番です。
HTML + CSS – アコーディオンの実装
<!-- HTML -->
<div class="accordion">
<button class="accordion-header">
アコーディオンのタイトル
</button>
<div class="accordion-body">
<div class="accordion-content">
<p>アコーディオンの中身...</p>
</div>
</div>
</div>
/* CSS */
.accordion-body {
max-height: 0;
overflow: hidden;
transition: max-height 0.4s ease-out;
}
.accordion.open .accordion-body {
max-height: 500px; /* 十分大きな値 */
}
JavaScript – アコーディオンの動的 height 制御
// より精密な高さ制御
document.querySelectorAll('[data-accordion]').forEach(accordion => {
const header = accordion.querySelector('[data-accordion-header]');
const body = accordion.querySelector('[data-accordion-body]');
const content = body.querySelector('[data-accordion-content]');
header.addEventListener('click', () => {
const isOpen = accordion.classList.contains('open');
if (isOpen) {
// 閉じる:現在の高さを設定してから 0 にする
body.style.maxHeight = body.scrollHeight + 'px';
requestAnimationFrame(() => {
body.style.maxHeight = '0';
});
accordion.classList.remove('open');
} else {
// 開く:コンテンツの高さを設定
body.style.maxHeight = content.scrollHeight + 'px';
accordion.classList.add('open');
// transition 完了後に max-height を解除(リサイズ対応)
body.addEventListener('transitionend', () => {
body.style.maxHeight = 'none';
}, { once: true });
}
});
});
ポイント:JavaScript版では、閉じるときに一度scrollHeightの値を設定してからrequestAnimationFrameで0にすることで、none→0(transitionなし)ではなく具体値→0(transitionあり)の変化を作り出しています。
animation が効かない原因①:@keyframes の定義ミス
animationが動かない場合、まず確認すべきは@keyframesの定義です。名前のタイポ、構文エラー、スコープの問題など、基本的なミスが原因であることが多いです。
@keyframes の名前間違い・タイポ
animation-nameと@keyframesの名前が一致していないと、アニメーションは一切動作しません。大文字・小文字も区別されます。
CSS – NG:名前が一致していない
/* @keyframes の名前は fadeIn(大文字I) */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.element {
/* fadein(小文字i)→ 名前が違うので動かない */
animation: fadein 0.5s ease;
}
CSS – OK:名前を一致させる
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.element {
animation: fadeIn 0.5s ease; /* 大文字小文字を一致させる */
}
from/to vs パーセント指定
from/toと0%/100%は同等ですが、混在させると混乱の原因になります。また、中間キーフレームのパーセント値に誤りがあると意図しない動きになります。
CSS – @keyframes の書き方パターン
/* from/to パターン(2点間の遷移) */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* パーセント パターン(複数の中間状態) */
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
/* NG: % の付け忘れ(無視される) */
@keyframes broken {
0 { opacity: 0; } /* NG: % がない */
100 { opacity: 1; } /* NG: % がない */
}
@keyframes がスコープ外にある
CSS Modules や Shadow DOM を使っている場合、@keyframesがコンポーネントのスコープ外に定義されていると、animation-nameから参照できません。
CSS Modules – @keyframes のスコープ問題
/* styles.module.css */
/* CSS Modules では @keyframes もスコープ化される */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.element {
/* CSS Modules では自動的にスコープ化された名前に変換される */
animation-name: fadeIn;
animation-duration: 0.5s;
}
/* グローバルに定義したい場合 */
:global {
@keyframes globalFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
}
animation-name の指定忘れ
ショートハンドのanimationプロパティでanimation-nameを忘れる、または間違った位置に書いてしまうケースです。
CSS – animation-name の指定ミス
/* NG: animation-name がない */
.element {
animation: 0.5s ease; /* 名前がない! */
}
/* OK: animation-name を正しく指定 */
.element {
animation: fadeIn 0.5s ease;
}
animation が効かない原因②:animation プロパティの設定ミス
@keyframesの定義が正しくても、animationプロパティの設定に問題があると動作しません。デフォルト値の罠やショートハンドの記法ミスが主な原因です。
animation-duration: 0s(デフォルト)
animation-durationのデフォルト値は0sです。animation-nameだけ指定してanimation-durationを忘れると、アニメーションは0秒で完了する(=見えない)ことになります。
CSS – NG:duration の指定忘れ
/* NG: animation-duration が 0s(デフォルト)のまま */
.element {
animation-name: fadeIn;
/* animation-duration: 0s; ← これがデフォルト */
}
/* OK: animation-duration を明示的に指定 */
.element {
animation-name: fadeIn;
animation-duration: 0.5s;
}
animation-fill-mode の挙動
animation-fill-modeは、アニメーション開始前と終了後にどのスタイルを適用するかを決定します。この設定を間違えると、アニメーション自体は動いているのに「効いていないように見える」ことがあります。
| fill-mode |
開始前 |
終了後 |
よくある問題 |
none(デフォルト) |
要素本来のスタイル |
要素本来のスタイルに戻る |
アニメーション後に元に戻ってしまう |
forwards |
要素本来のスタイル |
最後のキーフレームを保持 |
最もよく使われる設定 |
backwards |
最初のキーフレームを適用 |
要素本来のスタイルに戻る |
delay中に初期状態が表示される |
both |
最初のキーフレームを適用 |
最後のキーフレームを保持 |
delay + 保持が両方必要な場合 |
CSS – animation-fill-mode の問題と解決
/* NG: アニメーション後に元に戻ってしまう */
.fade-in {
opacity: 0;
animation: fadeIn 0.5s ease;
/* fill-mode: none → アニメーション後 opacity: 0 に戻る */
}
/* OK: forwards で最終状態を保持 */
.fade-in {
opacity: 0;
animation: fadeIn 0.5s ease forwards;
}
animation-play-state: paused
animation-play-stateがpausedになっていると、アニメーションは定義されているものの再生されません。JavaScriptで一時停止している場合や、CSSの継承で意図せずpausedになっている場合があります。
CSS – animation-play-state の確認
/* 意図せず paused になっているケース */
.parent {
animation-play-state: paused; /* 親に設定されている */
}
.parent .child {
animation: spin 1s linear infinite;
/* 親の paused を継承して動かない可能性 */
}
/* 確実に再生する */
.parent .child {
animation: spin 1s linear infinite;
animation-play-state: running;
}
ショートハンド記法の順序問題
animationショートハンドでは、時間値が2つある場合、1つ目がduration、2つ目がdelayとして解釈されます。順序を間違えると意図しない動作になります。
CSS – animation ショートハンドの順序
/* ショートハンドの構文 */
animation: [name] [duration] [timing-function] [delay]
[iteration-count] [direction] [fill-mode] [play-state];
/* NG: 0.5s が delay、1s が duration だと思っていたが逆 */
animation: fadeIn 0.5s ease 1s;
/* → duration: 0.5s, delay: 1s と解釈される */
/* OK: 意図を明確にするなら個別指定が安全 */
animation-name: fadeIn;
animation-duration: 0.5s;
animation-timing-function: ease;
animation-delay: 1s;
animation-fill-mode: forwards;
ポイント:ショートハンドで迷ったら、個別プロパティで指定しましょう。コードの可読性も上がり、デバッグも容易になります。特にanimation-fill-modeの指定忘れが「効かない」と感じる最大の原因です。
animation が効かない原因③:prefers-reduced-motion
OSやブラウザの設定で「動きを減らす」(prefers-reduced-motion)が有効になっていると、CSSフレームワークやリセットCSSがすべてのアニメーションを無効化している場合があります。
ユーザーのアクセシビリティ設定
以下の設定でprefers-reduced-motion: reduceが有効になります。
| OS |
設定場所 |
| Windows |
設定 → アクセシビリティ → 視覚効果 → アニメーション効果 |
| macOS |
システム環境設定 → アクセシビリティ → ディスプレイ → 動きを減らす |
| iOS |
設定 → アクセシビリティ → 動作 → 視差効果を減らす |
| Android |
設定 → ユーザー補助 → アニメーションを無効化 |
メディアクエリでの制御
多くのCSSフレームワーク(Bootstrap、Tailwind CSSなど)やリセットCSSは、以下のようなメディアクエリですべてのアニメーションを無効化しています。
CSS – prefers-reduced-motion のリセット(フレームワーク等に含まれる)
/* Bootstrap や Tailwind CSS に含まれるリセット */
@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;
}
}
この設定が有効な環境では、開発者が設定したすべてのtransition/animationがほぼ瞬時に完了します。開発中にアニメーションが動かない場合、まず自分のOSのアクセシビリティ設定を確認してください。
アニメーション無効化の適切な対応
アクセシビリティを考慮しつつ、必要なアニメーションを維持する方法を見てみましょう。
CSS – prefers-reduced-motion への適切な対応
/* 方法1: 動きが不要な場合のみ無効化 */
.decorative-animation {
animation: float 3s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.decorative-animation {
animation: none;
}
}
/* 方法2: 動きを許可するユーザーのみアニメーション適用 */
.safe-animation {
/* デフォルトではアニメーションなし */
opacity: 1;
}
@media (prefers-reduced-motion: no-preference) {
.safe-animation {
animation: fadeIn 0.5s ease forwards;
opacity: 0;
}
}
/* 方法3: JavaScriptで検出 */
/* const prefersReduced = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches; */
ポイント:方法2の「prefers-reduced-motion: no-preferenceでのみアニメーション適用」が、アクセシビリティのベストプラクティスです。デフォルトはアニメーションなしで、動きを好むユーザーのみアニメーションが適用されます。
共通の原因:CSSの詳細度・上書き問題
transitionもanimationも、他のCSSルールによって上書きされていると効かなくなります。特に、CSSフレームワークやサードパーティのライブラリと自分のCSSが競合するケースは非常に多いです。
他のCSSで上書きされている
CSSの詳細度(Specificity)によって、後から書いたスタイルや詳細度の高いセレクタがアニメーション関連のプロパティを上書きしている可能性があります。
CSS – 詳細度による上書きの例
/* 自分のCSS:transition を設定 */
.button {
transition: background-color 0.3s ease;
background-color: #3b82f6;
}
/* フレームワークのCSS:transition を none に上書き */
.container .button {
transition: none; /* 詳細度が高いため上書きされる */
}
CSS詳細度の計算ルール
- インラインスタイル: 1,0,0,0
- IDセレクタ (#id): 0,1,0,0
- クラスセレクタ (.class): 0,0,1,0
- 要素セレクタ (div): 0,0,0,1
- 数値が大きい方が優先される
- 同じ詳細度なら後に書かれた方が優先される
!important との戦い
!importantが付いたスタイルは、通常の詳細度を無視して最優先で適用されます。フレームワークやプラグインが!importantを使っていると、自分のtransition/animationが完全に無効化されることがあります。
CSS – !important による上書き問題
/* フレームワークのリセットCSS */
* {
transition: none !important; /* すべてのtransitionを無効化 */
}
/* 対策1: 同じく !important で上書き(非推奨だが応急措置) */
.button {
transition: background-color 0.3s ease !important;
}
/* 対策2: CSSの読み込み順序を調整 */
/* 自分のCSSをフレームワークの後に読み込む */
/* 対策3: @layer でカスケード層を制御(モダンな方法) */
@layer framework, custom;
@layer framework {
/* フレームワークのスタイル */
}
@layer custom {
/* 自分のスタイル(こちらが優先される) */
.button {
transition: background-color 0.3s ease;
}
}
メディアクエリ内での上書き
レスポンシブ対応のメディアクエリ内でアニメーション設定を意図せず上書きしていないか確認しましょう。
CSS – メディアクエリでの上書き問題
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
/* モバイルでは transition を消してしまっている */
@media (max-width: 768px) {
.sidebar {
position: fixed;
/* transition の指定を忘れている → モバイルでアニメーションしない */
}
}
/* 修正:メディアクエリ内でも transition を維持 */
@media (max-width: 768px) {
.sidebar {
position: fixed;
transition: transform 0.3s ease;
}
}
CSSフレームワークとの競合
Bootstrap、Tailwind CSS、Material UIなどのCSSフレームワークは、独自のtransition/animationを定義しています。これらが自分のCSSと競合するケースの対処法をまとめます。
| フレームワーク |
競合しやすいポイント |
対処法 |
| Bootstrap |
独自のtransition(.btn, .collapse等) |
Bootstrapのクラスを使うか、詳細度を上げる |
| Tailwind CSS |
preflightリセット、utility classes |
@layerでカスケード層を設定 |
| Material UI |
インラインスタイルでの上書き |
sx propやstyled()でカスタマイズ |
| Cocoon(WordPress) |
テーマのtransition設定 |
子テーマのCSSで上書き |
DevTools – 上書きの確認方法
/* Chrome DevTools での確認手順 */
1. F12 で DevTools を開く
2. 対象の要素を選択(Elements パネル)
3. Styles パネルで transition / animation を検索
4. 取り消し線が引かれているスタイルは上書きされている
5. Computed パネルで最終的に適用されている値を確認
共通の原因:ブラウザ互換性
モダンなCSSアニメーション機能は主要ブラウザで広くサポートされていますが、一部の新機能やプロパティでは互換性の問題が残っています。
ベンダープレフィックス(-webkit-)
現在のモダンブラウザではtransitionとanimationのベンダープレフィックスはほぼ不要です。しかし、古いSafari(iOS 8以前)や特定のWebView環境では-webkit-プレフィックスが必要な場合があります。
CSS – ベンダープレフィックス(古い環境向け)
/* 古い環境への対応が必要な場合 */
@-webkit-keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.element {
-webkit-animation: fadeIn 0.5s ease forwards;
animation: fadeIn 0.5s ease forwards;
}
ポイント:2024年現在、transitionとanimationの基本的なプロパティにベンダープレフィックスは不要です。ただし、ビルドツール(PostCSS + Autoprefixer)を使えば、必要に応じて自動的にプレフィックスを付与してくれます。
新しい機能のサポート状況
以下のアニメーション関連の新機能は、ブラウザによってサポート状況が異なります。
| 機能 |
Chrome |
Firefox |
Safari |
@starting-style |
117+ |
129+ |
17.4+ |
transition: display allow-discrete |
117+ |
129+ |
17.4+ |
animation-timeline |
115+ |
未対応 |
未対応 |
view-timeline |
115+ |
未対応 |
未対応 |
@layer |
99+ |
97+ |
15.4+ |
grid-template-rows: 0fr → 1fr |
111+ |
118+ |
17+ |
Can I Use での確認方法
ブラウザの互換性を確認するには、Can I Useが最も信頼できるリソースです。
確認すべきキーワード
Can I Use で検索するキーワード:
css-transitions → transition の基本サポート
css-animation → animation の基本サポート
css-starting-style → @starting-style のサポート
css-cascade-layers → @layer のサポート
mdn-css_properties_transition-behavior → allow-discrete
実装パターン集
ここまで学んだ「効かない原因」を踏まえた、実務で使える正しい実装パターンを紹介します。すべてのパターンはコピペで使え、主要ブラウザで動作確認済みです。
フェードイン / フェードアウト
最も基本的なアニメーションパターンです。visibility + opacityの組み合わせで、アクセシビリティにも配慮した実装です。
CSS – フェードイン / フェードアウト
/* CSS-only フェードパターン */
.fade-element {
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.3s ease,
visibility 0.3s ease;
}
.fade-element.visible {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
/* ページ読み込み時のフェードイン(animation版) */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in-up {
opacity: 0;
animation: fadeInUp 0.6s ease forwards;
}
/* 複数要素を順番にフェードイン */
.fade-in-up:nth-child(1) { animation-delay: 0.1s; }
.fade-in-up:nth-child(2) { animation-delay: 0.2s; }
.fade-in-up:nth-child(3) { animation-delay: 0.3s; }
スライドイン / スライドアウト
サイドバーやドロワーメニューでよく使われるパターンです。transformを使うとGPUアクセラレーションが効いてスムーズに動作します。
CSS – スライドイン / スライドアウト(ドロワーメニュー)
/* --- オーバーレイ --- */
.drawer-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease,
visibility 0.3s ease;
z-index: 999;
}
.drawer-overlay.active {
opacity: 1;
visibility: visible;
}
/* --- ドロワー本体 --- */
.drawer {
position: fixed;
top: 0;
left: 0;
width: 300px;
height: 100vh;
background: #fff;
transform: translateX(-100%);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 1000;
}
.drawer.active {
transform: translateX(0);
}
アコーディオン(height: auto対応)
CSS Grid のgrid-template-rowsを使った、JavaScriptなしでコンテンツの高さに自動対応するアコーディオンです。
HTML + CSS – CSS Grid アコーディオン
<!-- HTML -->
<details class="accordion">
<summary>アコーディオンのタイトル</summary>
<div class="accordion-body">
<div class="accordion-inner">
<p>アコーディオンのコンテンツ...</p>
</div>
</div>
</details>
/* CSS */
.accordion {
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
}
.accordion summary {
padding: 16px;
cursor: pointer;
font-weight: 600;
user-select: none;
}
.accordion-body {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s ease;
}
.accordion[open] .accordion-body {
grid-template-rows: 1fr;
}
.accordion-inner {
overflow: hidden;
}
ホバーエフェクト
カードやボタンのホバー時に使えるエフェクト集です。transformとbox-shadowを組み合わせたパフォーマンスの良いパターンです。
CSS – ホバーエフェクト集
/* リフトアップ(浮き上がり)エフェクト */
.card-lift {
transition: transform 0.2s ease,
box-shadow 0.2s ease;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.card-lift:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
}
/* スケールエフェクト */
.card-scale {
transition: transform 0.2s ease;
}
.card-scale:hover {
transform: scale(1.02);
}
/* ボーダーカラー変更 */
.card-border {
border: 2px solid transparent;
transition: border-color 0.2s ease;
}
.card-border:hover {
border-color: #3b82f6;
}
/* ボタンの押し込みエフェクト */
.btn-press {
transition: transform 0.1s ease;
}
.btn-press:active {
transform: scale(0.95);
}
ローディングスピナー
animationの無限ループを使った、定番のローディングスピナーです。
CSS – ローディングスピナー
/* シンプルなスピナー */
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e2e8f0;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
/* ドットスピナー */
@keyframes dotPulse {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
.dot-spinner {
display: flex;
gap: 8px;
}
.dot-spinner span {
width: 12px;
height: 12px;
background: #3b82f6;
border-radius: 50%;
animation: dotPulse 1.4s ease-in-out infinite both;
}
.dot-spinner span:nth-child(1) { animation-delay: -0.32s; }
.dot-spinner span:nth-child(2) { animation-delay: -0.16s; }
スケルトンスクリーン
コンテンツ読み込み中に表示するスケルトンローダーです。background-sizeとbackground-positionのアニメーションで光沢感を演出します。
CSS – スケルトンスクリーン
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.skeleton {
background: linear-gradient(
90deg,
#e2e8f0 25%,
#f1f5f9 37%,
#e2e8f0 63%
);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
border-radius: 4px;
}
/* 使い方 */
.skeleton-title {
width: 60%;
height: 24px;
margin-bottom: 12px;
}
.skeleton-text {
width: 100%;
height: 16px;
margin-bottom: 8px;
}
.skeleton-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
}
DevTools でのデバッグ
CSSアニメーションが効かないとき、Chrome DevToolsを使えば原因を素早く特定できます。ここでは、実践的なデバッグ手法を紹介します。
Chrome DevTools の Animations パネル
Chrome DevToolsには専用のAnimations パネルがあり、ページ上のすべてのアニメーションを視覚的に確認・制御できます。
Animations パネルの開き方
- F12 → DevTools を開く
- 右上の …(More tools)→ Animations を選択
- または Ctrl + Shift + P → 「Animations」と入力して検索
- パネルが開いたら、ページ上でアニメーションが発生すると自動的にキャプチャされる
Animations パネルの機能
Animations パネルでできること:
1. アニメーションのキャプチャ
- ページ上のすべての transition/animation を記録
- タイムラインで発火タイミングを確認
2. 再生速度の変更
- 25% / 10% に減速して詳細な動きを確認
- 一時停止してフレームごとに確認
3. タイミングの編集
- ドラッグで duration や delay を調整
- イージングカーブをビジュアルに編集
4. アニメーションの再生
- キャプチャしたアニメーションを何度でも再生
- フレームバイフレームで確認
transition のタイミング確認
ElementsパネルのStylesタブでtransitionプロパティの隣に表示されるイージングカーブアイコンをクリックすると、タイミング関数を視覚的に確認・編集できます。
DevTools – transition のデバッグ手順
transition が効かない場合のデバッグ手順:
Step 1: 要素を選択し、Computed タブで確認
- transition-property が期待する値になっているか
- transition-duration が 0s ではないか
- 取り消し線が引かれていないか(上書きされていないか)
Step 2: Styles タブで優先度を確認
- どのセレクタから transition が適用されているか
- !important が付いていないか
- 他のルールで transition: none が設定されていないか
Step 3: 対象プロパティの値を確認
- 変化前と変化後の値が両方明示されているか
- auto になっていないか
- display: none が使われていないか
Step 4: Console でテスト
- $0.style.transition で現在の inline style を確認
- getComputedStyle($0).transition で計算後の値を確認
animation の一時停止・スロー再生
DevTools の Console から JavaScript で直接アニメーションを制御できます。
JavaScript – DevTools Console でのアニメーション制御
// 要素を選択した状態で($0 = 選択中の要素)
// アニメーション情報を取得
$0.getAnimations();
// すべてのアニメーションを一時停止
document.getAnimations().forEach(a => a.pause());
// すべてのアニメーションを再生
document.getAnimations().forEach(a => a.play());
// アニメーション速度を25%に
document.getAnimations().forEach(a => {
a.playbackRate = 0.25;
});
// 特定の要素のアニメーション詳細を確認
const animations = $0.getAnimations();
animations.forEach(a => {
console.log('Name:', a.animationName);
console.log('Duration:', a.effect.getTiming().duration);
console.log('PlayState:', a.playState);
console.log('CurrentTime:', a.currentTime);
});
ポイント:getAnimations()はWeb Animations APIの一部で、ページ上のすべてのCSSアニメーションとtransitionを取得できます。playStateが"idle"の場合、アニメーションが正しく開始していない(@keyframesが見つからない等)可能性があります。
まとめ
CSSのtransition・animationが効かない原因は、大きく分けて以下のパターンに分類できます。原因の特定には、このチェックリストを上から順に確認するのが最も効率的です。
原因チェックリスト
transition が効かない場合のチェックリスト
- 対象のプロパティはアニメーション可能か?(display, position は不可)
- 変化前の値は明示的に設定されているか?(auto ではないか)
transition-duration が 0s ではないか?
- 同一フレーム内で値を変更していないか?(reflow / ダブルRAF で解決)
display: none からの復帰ではないか?(visibility + opacity で代替)
- 他のCSSルールで上書きされていないか?(DevToolsで確認)
prefers-reduced-motion が有効ではないか?
animation が効かない場合のチェックリスト
@keyframes の名前と animation-name は一致しているか?(大文字小文字含む)
@keyframes の構文は正しいか?(% の付け忘れ等)
animation-duration は 0s ではないか?(デフォルトは 0s)
animation-fill-mode は正しいか?(forwards を忘れていないか)
animation-play-state が paused になっていないか?
- ショートハンドの値の順序は正しいか?
@keyframes がスコープ内にあるか?(CSS Modules 等)
prefers-reduced-motion が有効ではないか?
transition vs animation 使い分けフローチャート
| 要件 |
推奨 |
理由 |
| ホバー/フォーカスでの変化 |
transition |
状態変化のトリガーがある |
| クラスの付け外しで変化 |
transition |
JSからの制御がシンプル |
| ページ読み込み時のアニメーション |
animation |
トリガー不要で自動再生 |
| 無限ループ(ローディング等) |
animation |
iteration-count: infinite |
| 複数の中間状態がある |
animation |
@keyframes でパーセント指定 |
| 単純な2点間の遷移 |
transition |
コードがシンプル |
| アニメーション後に元に戻す |
transition |
トリガー解除で自動的に戻る |
| スクロール連動アニメーション |
animation |
animation-timeline で制御可能 |
パフォーマンスのベストプラクティス(GPU対応プロパティ)
アニメーションのパフォーマンスは、対象プロパティによって大きく異なります。以下のプロパティはGPUアクセラレーションが効くため、60fpsのスムーズなアニメーションを実現できます。
| パフォーマンスレベル |
プロパティ |
処理内容 |
| 最速(Composite Only) |
transform, opacity |
GPUで合成処理のみ(Layout/Paintを回避) |
| 高速(Composite Only) |
filter, backdrop-filter |
GPUで合成処理(一部Paint発生) |
| 中速(Paint) |
background-color, color, box-shadow |
再描画が発生(Layoutは回避) |
| 低速(Layout) |
width, height, top, left, margin, padding |
レイアウト再計算 + 再描画 + 合成(最もコストが高い) |
CSS – パフォーマンスの良いアニメーション例
/* NG: top/left をアニメーション(Layout発生) */
.move-bad {
position: absolute;
top: 0;
left: 0;
transition: top 0.3s, left 0.3s;
}
.move-bad:hover {
top: 100px;
left: 100px;
}
/* OK: transform を使用(Composite Only) */
.move-good {
transition: transform 0.3s;
}
.move-good:hover {
transform: translate(100px, 100px);
}
/* NG: width/height をアニメーション(Layout発生) */
.resize-bad {
width: 100px;
height: 100px;
transition: width 0.3s, height 0.3s;
}
/* OK: transform: scale を使用(Composite Only) */
.resize-good {
transition: transform 0.3s;
}
.resize-good:hover {
transform: scale(1.5);
}
/* GPU レイヤーを明示的に作成(パフォーマンス向上) */
.gpu-accelerated {
will-change: transform;
/* または transform: translateZ(0); */
}
注意:will-changeはメモリを消費するため、常にすべての要素に設定するのは避けてください。アニメーションが実際に発生する要素にのみ設定し、アニメーション完了後にwill-change: autoに戻すのがベストプラクティスです。
パフォーマンス最適化のまとめ
- 位置の移動には
top/left ではなく transform: translate() を使う
- サイズ変更には
width/height ではなく transform: scale() を使う
- 表示/非表示には
display ではなく opacity + visibility を使う
- アニメーション対象の要素に
will-change を必要に応じて設定する
- 同時にアニメーションする要素数を最小限に抑える
- Chrome DevTools の Performance パネルでフレームレートを確認する
CSSアニメーションのトラブルは、仕組みを理解すれば必ず解決できます。この記事のチェックリストと解決パターンを参考に、スムーズで美しいアニメーションを実装してください。