CSSアニメーションとトランジションの実装ガイド|パフォーマンスを意識した動きの作り方


はじめに

前回の記事ではWebフォームのユーザビリティを高めるCSS・JavaScriptのテクニックとして、フローティングラベル・リアルタイムバリデーション・送信フィードバックなどを解説しました。その実装の随所にトランジションやアニメーションが登場しました。今回はCSSのトランジションとアニメーションの使い方を改めて整理して、パフォーマンスを意識した実装方法を解説します。この連載でこれまで登場してきた浮遊アニメーション・フェードイン・ホバーエフェクトを振り返りながら、動きの作り方を体系的にまとめます。


1. transitionとanimationの違い

1.1 2つの仕組みを整理する

CSSで動きを作る方法は大きく2つあります。

比較transitionanimation
動作のきっかけ状態変化(ホバー・クラス追加など)自動で開始・ループも可能
定義場所変化元の要素に書く@keyframes で定義して要素に適用
制御の複雑さシンプル(開始・終了の2点)複雑(複数の中間地点を定義できる)
向いている用途ホバー・フォーカス・状態変化ローディング・ループ・複雑な演出
/* transition:状態変化に対して動きを付ける */
.btn {
    background-color: #1A5FC0;
    transition: background-color 0.2s ease;  /* ここに書く */
}

.btn:hover {
    background-color: #0D3A7A;  /* この変化にtransitionが適用される */
}

/* animation:@keyframesで自律的な動きを定義する */
@keyframes fadeIn {
    from { opacity: 0; transform: translateY( 16px ); }
    to   { opacity: 1; transform: translateY( 0 );    }
}

.element {
    animation: fadeIn 0.6s ease forwards;
}

2. transitionの書き方と使い方

2.1 transitionの構文

/* 個別プロパティで書く場合 */
.element {
    transition-property:        background-color;
    transition-duration:        0.3s;
    transition-timing-function: ease;
    transition-delay:           0s;
}

/* ショートハンドで書く場合 */
.element {
    transition: background-color 0.3s ease 0s;
    /* プロパティ名  時間   タイミング関数  遅延 */
}

/* 複数プロパティを指定する(カンマ区切り) */
.element {
    transition:
        background-color 0.2s ease,
        transform        0.25s ease,
        box-shadow       0.25s ease;
}

/* all を使うとすべてのプロパティに適用(推奨しない) */
.element {
    transition: all 0.3s ease; /* ❌ パフォーマンス上問題になりやすい */
}

⚠️ transition: all は使わないようにしましょう: all を指定するとすべてのプロパティの変化を監視するためブラウザの負荷が増えます。必要なプロパティだけを個別に指定するのが正しい書き方です。

2.2 タイミング関数の使い分け

/* 代表的なタイミング関数 */
.natural   { transition-timing-function: ease; }           /* 自然な動き(デフォルト) */
.linear    { transition-timing-function: linear; }         /* 一定速度 */
.ease-in   { transition-timing-function: ease-in; }        /* 徐々に加速 */
.ease-out  { transition-timing-function: ease-out; }       /* 徐々に減速 */
.ease-both { transition-timing-function: ease-in-out; }    /* 加速→減速 */

/* cubic-bezierで自分で曲線を定義する */
.custom    { transition-timing-function: cubic-bezier( 0.34, 1.56, 0.64, 1 ); }
/* → spring(ばね)のような弾む動き */

/* CSS Easingキーワード(新しい書き方) */
.spring    { transition-timing-function: linear( 0, 0.5 25%, 1 ); }
タイミング関数向いている用途
ease汎用。ほとんどのUI変化
ease-out要素の出現(上から降りてくる・フェードイン)
ease-in要素の退場(フェードアウト・スクロールアウト)
ease-in-outモーダルの開閉・サイドバーのスライド
linearローテーション・プログレスバー
cubic-bezier個性的な動き・弾む演出

3. animationの書き方と使い方

3.1 @keyframesの定義

/* fromとtoで書く(シンプルな場合) */
@keyframes fadeInUp {
    from {
        opacity:   0;
        transform: translateY( 24px );
    }
    to {
        opacity:   1;
        transform: translateY( 0 );
    }
}

/* パーセントで書く(複数の中間地点が必要な場合) */
@keyframes bounce {
    0%   { transform: translateY( 0 ); }
    30%  { transform: translateY( -20px ); }
    50%  { transform: translateY( 0 ); }
    70%  { transform: translateY( -10px ); }
    100% { transform: translateY( 0 ); }
}

/* 複数プロパティを同時に変化させる */
@keyframes slideInLeft {
    from {
        opacity:   0;
        transform: translateX( -32px );
        filter:    blur( 4px );
    }
    to {
        opacity:   1;
        transform: translateX( 0 );
        filter:    blur( 0 );
    }
}

3.2 animationプロパティの書き方

/* 個別プロパティ */
.element {
    animation-name:            fadeInUp;
    animation-duration:        0.6s;
    animation-timing-function: ease-out;
    animation-delay:           0.1s;
    animation-iteration-count: 1;        /* infinite でループ */
    animation-direction:       normal;   /* alternate で往復 */
    animation-fill-mode:       forwards; /* 終了後の状態を維持する */
    animation-play-state:      running;  /* paused で一時停止 */
}

/* ショートハンド */
.element {
    animation: fadeInUp 0.6s ease-out 0.1s 1 normal forwards;
    /* 名前      時間   タイミング 遅延 回数  方向   fill-mode */
}

/* 複数アニメーションを組み合わせる */
.element {
    animation:
        fadeInUp  0.6s ease-out forwards,
        float     3s   ease-in-out 0.6s infinite;
}

💡 animation-fill-mode: forwards は忘れず設定しましょう: これを設定しないとアニメーション終了後に要素が開始前の状態(opacity: 0 など)に戻ってしまいます。フェードインなど「出現して終わり」のアニメーションには必ず forwards を付けます。


4. パフォーマンスを意識した実装

4.1 GPUで処理できるプロパティを使う

CSSプロパティには「CPUで処理するもの」と「GPUで処理できるもの」があります。GPUで処理できるプロパティを使うと滑らかなアニメーションが実現できます。

プロパティ処理パフォーマンス
transformGPU✅ 最適
opacityGPU✅ 最適
filterGPU(一部)✅ 概ね良好
widthheightCPU(リフロー発生)❌ 避ける
topleftCPU(リフロー発生)❌ 避ける
marginpaddingCPU(リフロー発生)❌ 避ける
background-colorCPU(リペイント)⚠️ 多用を避ける
/* ❌ パフォーマンスが悪い実装 */
.bad-animation {
    transition: width 0.3s ease, top 0.3s ease;
}

.bad-animation:hover {
    width: 200px;  /* リフロー発生 */
    top:   10px;   /* リフロー発生 */
}

/* ✅ パフォーマンスが良い実装 */
.good-animation {
    transition: transform 0.3s ease;
}

.good-animation:hover {
    transform: translateX( 10px ) scale( 1.05 ); /* GPU処理 */
}

4.2 will-changeで事前通知する

/* アニメーション対象の要素にwill-changeを設定する */
.animated-element {
    will-change: transform, opacity;
}

will-change はブラウザに「このプロパティが変化することを事前に通知する」ヒントです。ブラウザが事前に最適化の準備をするため、アニメーション開始時のカクつきを減らせます。

⚠️ will-change は多用しないでください: すべての要素に設定するとメモリを大量に消費してかえってパフォーマンスが悪化します。実際にアニメーションが発生する要素に絞って使います。アニメーションが終わったら will-change: auto でリセットするのが理想です。

// JavaScriptと組み合わせて適切に管理する
const el = document.querySelector( '.animated-element' );

el.addEventListener( 'mouseenter', () => {
    el.style.willChange = 'transform';
} );

el.addEventListener( 'mouseleave', () => {
    el.style.willChange = 'auto'; /* 不要になったらリセット */
} );

4.3 アニメーションの数を減らす

/* ❌ 個別要素に別々のアニメーションを設定する */
.card-1 { animation: fadeIn 0.5s ease forwards; }
.card-2 { animation: fadeIn 0.5s ease 0.1s forwards; }
.card-3 { animation: fadeIn 0.5s ease 0.2s forwards; }
/* → 同じアニメーションを複数の要素に適用 */

/* ✅ CSSで遅延だけ変える(@keyframesは共通) */
.card {
    animation: fadeIn 0.5s ease forwards;
    opacity:   0; /* 初期状態 */
}

.card:nth-child(1) { animation-delay: 0s; }
.card:nth-child(2) { animation-delay: 0.1s; }
.card:nth-child(3) { animation-delay: 0.2s; }

5. この連載で登場したアニメーションを振り返る

5.1 浮遊アニメーション(第三十五弾)

/* X方向とY方向を分けて自然な浮遊感を出す */
@keyframes floating-x {
    0%, 100% { transform: translateX(  0px ); }
    50%       { transform: translateX( 12px ); }
}

@keyframes floating-y {
    0%, 100% { transform: translateY(  0px ); }
    50%       { transform: translateY( -16px ); }
}

.floating-element {
    animation:
        floating-x 7.2s ease-in-out infinite,
        floating-y 1.8s ease-in-out infinite;
}

5.2 フェードイン(Intersection Observer連携)

/* スクロールで要素が画面に入ったときにフェードイン */
.fade-in-view {
    opacity:    0;
    transform:  translateY( 24px );
    transition: opacity   0.7s ease,
                transform 0.7s ease;
}

.fade-in-view.is-visible {
    opacity:   1;
    transform: translateY( 0 );
}

/* カードを時間差で登場させる */
.card:nth-child(1) { transition-delay: 0s; }
.card:nth-child(2) { transition-delay: 0.1s; }
.card:nth-child(3) { transition-delay: 0.2s; }
/* Intersection Observer */
const observer = new IntersectionObserver( entries => {
    entries.forEach( entry => {
        if ( entry.isIntersecting ) {
            entry.target.classList.add( 'is-visible' );
            observer.unobserve( entry.target );
        }
    } );
}, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' } );

document.querySelectorAll( '.fade-in-view' )
        .forEach( el => observer.observe( el ) );

5.3 ホバーアニメーション(カード・ボタン)

/* カードのホバー */
.card {
    transition:
        transform  0.25s ease,
        box-shadow 0.25s ease;
    will-change: transform;
}

.card:hover {
    transform:  translateY( -6px );
    box-shadow: 0 12px 32px rgba( 0, 0, 0, 0.14 );
}

/* カード内画像のズーム */
.card__img {
    transition: transform 0.4s ease;
    overflow:   hidden;
}

.card:hover .card__img img {
    transform: scale( 1.04 );
}

/* ボタンのホバー */
.btn {
    transition:
        background-color 0.2s ease,
        transform        0.15s ease,
        box-shadow       0.2s ease;
}

.btn:hover {
    transform:  translateY( -2px );
    box-shadow: 0 6px 16px rgba( 26, 95, 192, 0.3 );
}

.btn:active {
    transform: translateY( 1px );
    box-shadow: none;
}

5.4 ローディングスピナー

/* 送信中に使ったスピナー */
@keyframes spin {
    from { transform: rotate(   0deg ); }
    to   { transform: rotate( 360deg ); }
}

.spinner {
    width:        20px;
    height:       20px;
    border:       2.5px solid rgba( 255, 255, 255, 0.3 );
    border-top:   2.5px solid white;
    border-radius: 50%;
    animation:    spin 0.7s linear infinite;
}

/* ドットローダー */
@keyframes dotBounce {
    0%, 80%, 100% { transform: scale( 0 );   opacity: 0; }
    40%            { transform: scale( 1 );   opacity: 1; }
}

.dot-loader span {
    display:          inline-block;
    width:            8px;
    height:           8px;
    border-radius:    50%;
    background:       currentColor;
    animation:        dotBounce 1.4s ease-in-out infinite;
}

.dot-loader span:nth-child(1) { animation-delay: 0s; }
.dot-loader span:nth-child(2) { animation-delay: 0.16s; }
.dot-loader span:nth-child(3) { animation-delay: 0.32s; }

6. アクセシビリティ:動きを減らす設定への対応

6.1 prefers-reduced-motionで動きを無効にする

前回までの記事でも登場しましたが、OSの「視差効果を減らす」設定を有効にしているユーザーに対してはアニメーションを無効化または軽減する対応が必要です。

/* 基本はアニメーションあり・設定に応じてOFFにする */
.fade-in-view {
    opacity:    0;
    transform:  translateY( 24px );
    transition: opacity   0.7s ease,
                transform 0.7s ease;
}

.fade-in-view.is-visible {
    opacity:   1;
    transform: translateY( 0 );
}

/* prefers-reduced-motionが有効なユーザーには動きを最小限に */
@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;
    }

    /* フェードインは瞬時に表示する(消えたままにしない) */
    .fade-in-view {
        opacity:   1 !important;
        transform: none !important;
    }
}

💡 アニメーションを「消す」のではなく「即時完了」に変えるのがポイントです: animation-duration: 0.01ms にすることでアニメーション自体は発生しますが、人間が感知できないほど速く完了します。opacity: 0 のままにしてしまうと、アニメーション無効のユーザーに要素が見えなくなってしまいます。


7. 動きのデザイン原則

7.1 適切な時間の目安

UIアニメーションの時間の目安:

マイクロインタラクション(ホバー・フォーカス):100〜200ms
状態変化(ボタン・フォーム):            200〜300ms
要素の出現・退場(フェード・スライド):    300〜500ms
ページ遷移・大きな演出:                  500〜800ms
ループアニメーション(浮遊・呼吸):        1000ms以上

⚠️ 1000ms(1秒)を超えるアニメーションはユーザーに「遅い」と感じさせる
⚠️ 100ms未満は人間が感知しにくいためUIアニメーションには不向き

7.2 アニメーションを使いすぎない

/* ❌ やりすぎのアニメーション */
.element {
    animation:
        fadeIn  0.5s ease forwards,
        bounce  1s   ease 0.5s,
        shake   0.3s ease 1.5s,
        glow    2s   ease 2s infinite;
}
/* → ユーザーの注意が散漫になる */

/* ✅ 目的のある最小限のアニメーション */
.element {
    animation: fadeIn 0.5s ease-out forwards;
}
/* → 出現をスムーズに伝えるだけ */

💡 「アニメーションは目的があるものだけ使う」が原則です: 動きはユーザーの注意を引く強力な手段であるがゆえに、使いすぎると本当に伝えたい情報が埋もれます。「ここに注目してほしい」「処理中である」「操作が完了した」など、明確な理由があるものだけに絞りましょう。


8. 実践チェックリスト

transformopacity を中心に使ってGPU処理を活かしているか

transition: all を使っていないか(プロパティを個別に指定しているか)

will-change をアニメーションが必要な要素に限定して使っているか

animation-fill-mode: forwards を付けて終了後の状態が保持されているか

各アニメーションの時間が用途に合った長さか(ホバー200ms・フェード500ms目安)

prefers-reduced-motion: reduce でアニメーションを無効化または軽減しているか

フェードインの is-visible 追加をIntersection Observerで制御しているか

ループアニメーション(スピナー・浮遊)が不要になったら停止する設計になっているか

Chrome DevToolsのPerformanceタブでアニメーション中のフレームレートを確認したか

アニメーションが「目的のある最小限」になっているか


まとめ

今回はCSSアニメーションとトランジションの書き方・パフォーマンスを意識した実装方法を解説しました。ポイントをまとめると:

  • transition は状態変化に動きを付けるもの・animation@keyframes で自律的な動きを定義するものと使い分ける
  • パフォーマンス上は transformopacity を中心に使う。widthtop などのレイアウトに影響するプロパティのアニメーションは避ける
  • transition: all は使わず変化させるプロパティを個別に指定する
  • will-change は必要な要素に限定して使いアニメーション終了後に auto でリセットする
  • animation-fill-mode: forwards を設定して終了後も最終フレームの状態を維持する
  • prefers-reduced-motion: reduce でアニメーションを即時完了させてアクセシビリティに対応する
  • アニメーションの時間はホバーが100〜200ms・フェードインが300〜500msが目安
  • 動きには必ず目的を持たせ使いすぎない

次の記事では、JavaScriptの非同期処理(Promiseasync/awaitfetch)を改めて整理して、実践的なAPI通信の実装方法を解説します。お楽しみに!

コメント

タイトルとURLをコピーしました