はじめに

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-duration0sの場合、アニメーションは実行されますが、視覚的には表示されません。animationstartanimationendイベントは発火します。

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%]
    end

alternateを使用すると、アニメーションが滑らかに往復するため、自然な動きを表現できます。

animation-fill-mode

アニメーションの実行前後に、キーフレームで定義したスタイルを適用するかどうかを指定します。

説明
none アニメーション前後はスタイルを適用しない
forwards 終了後も最後のキーフレームのスタイルを維持
backwards 開始前(delay中)に最初のキーフレームのスタイルを適用
both forwardsbackwardsの両方を適用
 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アクセラレーションを活用する

transformopacityは、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つのサブプロパティを使い分けることで、細かな制御が可能
  • 複数アニメーションを組み合わせて複雑な表現を実現できる
  • transformopacityを優先的に使用してパフォーマンスを最適化する
  • prefers-reduced-motionでアクセシビリティに配慮した実装を心がける

次の記事では、transformプロパティを詳しく解説し、translaterotatescaleskewを使った変形エフェクトの実装方法を学びます。

参考リンク