はじめに#
Webサイトでローディングスピナーが回転したり、通知バッジがふわっと現れたり、カードが滑らかにスライドインするエフェクトを見たことがあるでしょう。これらの複雑で連続的なアニメーションを実現しているのがCSSキーフレームアニメーションです。
前回の記事で解説したトランジションは「状態Aから状態Bへの変化」を滑らかにする機能でした。一方、キーフレームアニメーションは、複数の中間状態を経由する複雑なアニメーションを定義できます。ページ読み込み時に自動再生したり、無限ループさせたりすることも可能です。
本記事では、CSSアニメーションの基礎から実践的なパターンまで、以下の内容を体系的に解説します。
@keyframesによるアニメーションシーケンスの定義
animationプロパティ群(8つのサブプロパティ)
- 複数アニメーションの組み合わせとカスケード
- パフォーマンス最適化のベストプラクティス
prefers-reduced-motionによるアクセシビリティ対応
トランジションとアニメーションの違い#
CSSには2つのアニメーション機能があります。それぞれの特徴を理解することで、適切な場面で使い分けられるようになります。
| 特徴 |
トランジション |
アニメーション |
| トリガー |
状態変化(:hover、クラス追加など)が必要 |
自動開始可能 |
| 中間状態 |
2つの状態間のみ |
複数のキーフレームを定義可能 |
| 繰り返し |
状態変化のたびに1回 |
無限ループ可能 |
| 制御 |
シンプル |
詳細な制御が可能 |
| 用途 |
ホバーエフェクト、フォーカス効果 |
ローディング、注目喚起、複雑な演出 |
graph LR
subgraph トランジション
A[状態A] -->|1回の変化| B[状態B]
end
subgraph アニメーション
C[0%] --> D[25%]
D --> E[50%]
E --> F[75%]
F --> G[100%]
G -->|繰り返し| C
end@keyframesによるアニメーション定義#
基本構文#
@keyframesアットルールは、アニメーションの流れを定義するための構文です。アニメーション名と、各時点でのスタイルを指定します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/* 基本構文 */
@keyframes アニメーション名 {
0% {
/* アニメーション開始時のスタイル */
}
50% {
/* 中間時点のスタイル */
}
100% {
/* アニメーション終了時のスタイル */
}
}
/* from/toキーワードを使用した書き方 */
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
|
キーフレームセレクタの種類#
キーフレームセレクタには、パーセント値またはキーワードを使用できます。
| セレクタ |
説明 |
from |
0%と同等。アニメーション開始時点 |
to |
100%と同等。アニメーション終了時点 |
<percentage> |
アニメーション全体の中での時点(0%〜100%) |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
25% {
transform: translateY(-30px);
}
50% {
transform: translateY(-15px);
}
75% {
transform: translateY(-5px);
}
}
|
複数プロパティのアニメーション#
1つのキーフレーム内で複数のプロパティを同時にアニメーションさせることができます。
1
2
3
4
5
6
7
8
9
10
|
@keyframes slide-in-fade {
from {
opacity: 0;
transform: translateX(-100px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
|
キーフレームの省略#
0%と100%を省略した場合、ブラウザは要素の既存スタイルをその時点のスタイルとして使用します。
1
2
3
4
5
6
7
8
|
/* 中間地点のみ定義 */
@keyframes pulse {
50% {
transform: scale(1.1);
opacity: 0.8;
}
}
/* 0%と100%は要素の通常スタイルが適用される */
|
animationプロパティ群#
アニメーションを要素に適用するには、animationプロパティを使用します。これは8つのサブプロパティの一括指定(ショートハンド)です。
一括指定(ショートハンド)#
1
2
3
4
5
6
7
8
9
10
11
12
|
/* 構文 */
animation: <name> <duration> <timing-function> <delay>
<iteration-count> <direction> <fill-mode> <play-state>;
/* 例:フェードインアニメーション */
animation: fade-in 0.5s ease-out;
/* 例:無限ループするバウンスアニメーション */
animation: bounce 1s ease-in-out infinite;
/* 例:すべてのプロパティを指定 */
animation: slide-in 0.3s ease-out 0.1s 1 normal forwards running;
|
各サブプロパティの詳細#
| プロパティ |
説明 |
既定値 |
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 |
animation-name#
アニメーションに使用する@keyframesの名前を指定します。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
.element {
animation-name: fade-in;
}
/* 複数のアニメーションを指定 */
.element {
animation-name: fade-in, slide-up;
}
/* アニメーションなし */
.element {
animation-name: none;
}
|
animation-duration#
アニメーションの1サイクルにかかる時間を秒(s)またはミリ秒(ms)で指定します。
1
2
3
4
5
|
.element {
animation-duration: 0.5s; /* 0.5秒 */
animation-duration: 500ms; /* 500ミリ秒 */
animation-duration: 2s; /* 2秒 */
}
|
animation-durationが0sの場合、アニメーションは実行されますが、視覚的には表示されません。animationstartとanimationendイベントは発火します。
animation-timing-function#
アニメーションの進行速度を制御するイージング関数を指定します。トランジションと同じ関数が使用できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
.element {
animation-timing-function: ease; /* ゆっくり始まり、速くなり、ゆっくり終わる */
animation-timing-function: linear; /* 一定速度 */
animation-timing-function: ease-in; /* ゆっくり始まる */
animation-timing-function: ease-out; /* ゆっくり終わる */
animation-timing-function: ease-in-out; /* ゆっくり始まり、ゆっくり終わる */
}
/* ステップ関数(コマ送りアニメーション) */
.element {
animation-timing-function: steps(4); /* 4段階で変化 */
animation-timing-function: step-start; /* 各ステップの開始時に変化 */
animation-timing-function: step-end; /* 各ステップの終了時に変化 */
}
/* カスタムベジェ曲線 */
.element {
animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
|
animation-delay#
アニメーションが開始するまでの待ち時間を指定します。負の値を指定すると、アニメーションの途中から開始したように見えます。
1
2
3
4
|
.element {
animation-delay: 0.5s; /* 0.5秒後に開始 */
animation-delay: -0.2s; /* 0.2秒経過した状態から開始 */
}
|
animation-iteration-count#
アニメーションの繰り返し回数を指定します。
1
2
3
4
5
6
|
.element {
animation-iteration-count: 1; /* 1回(既定値) */
animation-iteration-count: 3; /* 3回 */
animation-iteration-count: 2.5; /* 2.5回(途中で終了) */
animation-iteration-count: infinite; /* 無限ループ */
}
|
animation-direction#
アニメーションの再生方向を指定します。
| 値 |
説明 |
normal |
順方向に再生(0% → 100%) |
reverse |
逆方向に再生(100% → 0%) |
alternate |
順方向と逆方向を交互に再生 |
alternate-reverse |
逆方向から開始し、交互に再生 |
1
2
3
4
5
6
|
.element {
animation-direction: normal;
animation-direction: reverse;
animation-direction: alternate;
animation-direction: alternate-reverse;
}
|
graph LR
subgraph normal
A1[0%] --> A2[100%]
A2 --> A3[0%]
A3 --> A4[100%]
end
subgraph alternate
B1[0%] --> B2[100%]
B2 --> B3[0%]
B3 --> B4[100%]
endalternateを使用すると、アニメーションが滑らかに往復するため、自然な動きを表現できます。
animation-fill-mode#
アニメーションの実行前後に、キーフレームで定義したスタイルを適用するかどうかを指定します。
| 値 |
説明 |
none |
アニメーション前後はスタイルを適用しない |
forwards |
終了後も最後のキーフレームのスタイルを維持 |
backwards |
開始前(delay中)に最初のキーフレームのスタイルを適用 |
both |
forwardsとbackwardsの両方を適用 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
/* アニメーション終了後も最終状態を維持 */
.element {
animation: fade-in 0.5s ease-out forwards;
}
/* delay中も開始時のスタイルを適用 */
.element {
animation: slide-in 0.5s ease-out 1s backwards;
}
/* 両方適用 */
.element {
animation: fade-in 0.5s ease-out 0.5s both;
}
|
animation-play-state#
アニメーションの再生状態を制御します。JavaScriptと組み合わせてアニメーションを一時停止・再開できます。
1
2
3
4
5
6
7
8
9
|
.element {
animation-play-state: running; /* 再生中(既定値) */
animation-play-state: paused; /* 一時停止 */
}
/* ホバーでアニメーションを一時停止 */
.element:hover {
animation-play-state: paused;
}
|
複数アニメーションの組み合わせ#
カンマ区切りで複数指定#
1つの要素に複数のアニメーションを適用できます。
1
2
3
4
5
6
|
.element {
animation:
fade-in 0.5s ease-out,
slide-up 0.5s ease-out,
scale-in 0.3s ease-out 0.2s;
}
|
個別プロパティでの複数指定#
各サブプロパティも複数の値を受け取れます。
1
2
3
4
5
6
|
.element {
animation-name: fade-in, slide-up, scale-in;
animation-duration: 0.5s, 0.5s, 0.3s;
animation-timing-function: ease-out, ease-out, ease-out;
animation-delay: 0s, 0s, 0.2s;
}
|
値の数が一致しない場合、短い方のリストが繰り返されます。
1
2
3
4
|
.element {
animation-name: fade-in, slide-up, bounce;
animation-duration: 0.5s, 1s; /* bounceには0.5sが適用される */
}
|
アニメーションのカスケード#
同じプロパティに対して複数のアニメーションが適用された場合、後から宣言されたアニメーションが優先されます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@keyframes move-x {
to { transform: translateX(100px); }
}
@keyframes move-y {
to { transform: translateY(100px); }
}
.element {
/* move-yがtransformを上書きするため、Y方向のみ移動 */
animation:
move-x 1s ease,
move-y 1s ease;
}
|
この問題を回避するには、個別のtransform関数を使用するか、1つのキーフレームで両方の変形を定義します。
1
2
3
4
5
|
@keyframes move-xy {
to {
transform: translateX(100px) translateY(100px);
}
}
|
実践的なアニメーションパターン#
ローディングスピナー#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e0e0e0;
border-top-color: #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
|
パルスエフェクト(注目喚起)#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.8;
}
}
.notification-badge {
animation: pulse 2s ease-in-out infinite;
}
|
スライドインアニメーション#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@keyframes slide-in-right {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.card {
animation: slide-in-right 0.5s ease-out forwards;
}
/* 複数要素を順番にアニメーション */
.card:nth-child(1) { animation-delay: 0s; }
.card:nth-child(2) { animation-delay: 0.1s; }
.card:nth-child(3) { animation-delay: 0.2s; }
|
タイピングエフェクト#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@keyframes typing {
from { width: 0; }
to { width: 100%; }
}
@keyframes blink-caret {
50% { border-color: transparent; }
}
.typewriter {
overflow: hidden;
white-space: nowrap;
border-right: 3px solid #333;
animation:
typing 3s steps(30) forwards,
blink-caret 0.75s step-end infinite;
}
|
フェードイン・アウト切り替え#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@keyframes fade-in {
from { opacity: 0; display: none; }
to { opacity: 1; display: block; }
}
@keyframes fade-out {
from { opacity: 1; display: block; }
to { opacity: 0; display: none; }
}
.modal.show {
animation: fade-in 0.3s ease-out forwards;
}
.modal.hide {
animation: fade-out 0.3s ease-out forwards;
}
|
パフォーマンス最適化#
GPUアクセラレーションを活用する#
transformとopacityは、GPUで処理されるため、他のプロパティよりも高速にアニメーションできます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
/* 推奨:transform と opacity を使用 */
@keyframes optimized {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* 非推奨:レイアウトを引き起こすプロパティ */
@keyframes non-optimized {
from {
left: -100px; /* レイアウト再計算が発生 */
width: 0; /* レイアウト再計算が発生 */
}
to {
left: 0;
width: 200px;
}
}
|
will-changeプロパティ#
アニメーションするプロパティを事前にブラウザに通知することで、最適化を促すことができます。
1
2
3
4
5
6
7
8
9
|
.animated-element {
will-change: transform, opacity;
animation: slide-in 0.5s ease-out;
}
/* アニメーション完了後は解除 */
.animated-element.animation-done {
will-change: auto;
}
|
ただし、will-changeの過剰な使用はメモリ消費を増加させるため、必要な要素にのみ適用してください。
アニメーション可能なプロパティの選択#
パフォーマンスへの影響度でプロパティを分類すると、以下のようになります。
| パフォーマンス |
プロパティ例 |
| 最適(Composite) |
transform, opacity |
| 良好(Paint) |
color, background-color, box-shadow |
| 注意(Layout) |
width, height, margin, padding, top, left |
prefers-reduced-motionへの対応#
アクセシビリティの重要性#
一部のユーザーは、動きのあるコンテンツによって不快感や健康上の問題(前庭障害、片頭痛、てんかんなど)を経験することがあります。prefers-reduced-motionメディアクエリを使用することで、これらのユーザーに配慮したアニメーション設計が可能です。
基本的な実装パターン#
1
2
3
4
5
6
7
8
9
10
11
|
/* 通常のアニメーション */
.element {
animation: slide-in 0.5s ease-out;
}
/* 動きを減らす設定が有効な場合 */
@media (prefers-reduced-motion: reduce) {
.element {
animation: fade-in 0.2s ease-out; /* より控えめなアニメーションに変更 */
}
}
|
アニメーションを完全に無効化#
1
2
3
4
5
6
7
8
9
|
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
|
代替アニメーションの提供#
完全に無効化するのではなく、より穏やかなアニメーションに置き換える方法もあります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@keyframes pulse-standard {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2); }
}
@keyframes pulse-reduced {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.notification {
animation: pulse-standard 2s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.notification {
animation: pulse-reduced 4s ease-in-out infinite;
}
}
|
JavaScriptでの検出#
CSSだけでなく、JavaScriptでもユーザー設定を検出できます。
1
2
3
4
5
6
7
8
9
10
11
|
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
if (prefersReducedMotion) {
// 控えめなアニメーションを使用
element.classList.add('reduced-motion');
} else {
// 通常のアニメーションを使用
element.classList.add('full-motion');
}
|
JavaScriptとの連携#
アニメーションイベント#
CSSアニメーションの状態をJavaScriptで検出できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
const element = document.querySelector('.animated');
// アニメーション開始時
element.addEventListener('animationstart', (e) => {
console.log(`アニメーション開始: ${e.animationName}`);
});
// アニメーション終了時
element.addEventListener('animationend', (e) => {
console.log(`アニメーション終了: ${e.animationName}`);
element.classList.remove('animate');
});
// アニメーションの各反復終了時
element.addEventListener('animationiteration', (e) => {
console.log(`反復: ${e.elapsedTime}秒経過`);
});
|
クラスの動的追加によるアニメーション制御#
1
2
3
4
5
6
7
8
9
10
11
12
|
// アニメーションを開始
function startAnimation(element) {
element.classList.add('animate');
}
// アニメーションをリセットして再開
function restartAnimation(element) {
element.classList.remove('animate');
// リフローを強制して再アニメーションを可能にする
void element.offsetWidth;
element.classList.add('animate');
}
|
トラブルシューティング#
アニメーションが動作しない#
原因1: animation-durationが未指定または0s
1
2
3
4
5
6
7
8
9
10
|
/* 誤り */
.element {
animation-name: fade-in;
/* animation-durationが未指定 */
}
/* 正しい */
.element {
animation: fade-in 0.5s ease-out;
}
|
原因2: @keyframesの名前が一致しない
1
2
3
4
5
6
7
8
9
|
/* @keyframesの定義 */
@keyframes fadeIn { /* キャメルケース */
/* ... */
}
/* 適用(名前が一致していない) */
.element {
animation-name: fade-in; /* ケバブケース */
}
|
アニメーション終了後に要素が消える#
animation-fill-mode: forwardsを使用して、終了時のスタイルを維持します。
1
2
3
4
|
.element {
opacity: 0; /* 初期状態 */
animation: fade-in 0.5s ease-out forwards; /* 終了後もopacity: 1を維持 */
}
|
無限ループアニメーションがカクつく#
開始と終了のスタイルを一致させることで、スムーズなループを実現します。
1
2
3
4
5
6
7
8
9
10
11
|
/* カクつく */
@keyframes rotate-bad {
from { transform: rotate(0deg); }
to { transform: rotate(359deg); } /* 1度の差でカクつく */
}
/* スムーズ */
@keyframes rotate-smooth {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); } /* 完全な360度 */
}
|
まとめ#
CSSキーフレームアニメーションは、Webサイトに豊かな視覚表現を加えるための強力な機能です。本記事で学んだポイントを振り返りましょう。
@keyframesでアニメーションのシーケンスを定義し、animationプロパティで要素に適用する
- 8つのサブプロパティを使い分けることで、細かな制御が可能
- 複数アニメーションを組み合わせて複雑な表現を実現できる
transformとopacityを優先的に使用してパフォーマンスを最適化する
prefers-reduced-motionでアクセシビリティに配慮した実装を心がける
次の記事では、transformプロパティを詳しく解説し、translate、rotate、scale、skewを使った変形エフェクトの実装方法を学びます。
参考リンク#