FireShot というChrome拡張機能を使って、制作したWebページのフルページスクリーンショットを撮ったところ、ページがまるごと真っ白になった。

最初は単純なバグかと思ったが、原因を追っていくと「CSS animation の仕組みそのもの」が関係していた。そしてこの問題を解決するために、自作のアニメーションライブラリ一式を transition ベースに書き直すことになった。

その経緯と対処法をまとめておく。


    CSSアニメーションが原因だった

    自作の .chassis コンポーネントには、ページロード時のレイアウトシフト(CLS)を防ぐためのホワイトアウト演出がある。

    .chassis::after {
      content: "";
      position: fixed;
      inset: 0;
      background: white;
      animation: chassisAfterIn 1.8s ease 0.1s;
      animation-fill-mode: forwards;
    }
    
    @keyframes chassisAfterIn {
      100% { opacity: 0; }
    }

    ページが読み込まれると白いオーバーレイが表示され、1.8秒かけてフェードアウトする。ブラウザ上では正常に動作している。

    しかし FireShot でスクリーンショットを撮ると、真っ白な画像が保存されるだけだった。


    FireShot(の一部モード)は、Chromeの印刷エンジンを使ってページをキャプチャするらしい。

    印刷エンジンの特性として、CSSアニメーションはアニメーション開始前の初期フレームで固定される

    つまり .chassis::after は:

    • 通常のブラウザ → animation が再生され、1.8秒後に opacity: 0 になる ✅
    • FireShot(印刷エンジン) → animation が再生されず、初期状態(opacity: 1 = 白)のまま固定される ❌

    これが真っ白になる原因で、以前は問題が出ていなかった理由も判明した。.chassis::afteranimation を使い始めたのが比較的最近で、それ以前は別の方法(transitionベース)を使っていたからだ。


    解決策:animation → transition に切り替える

    .chassis::after については、transition + JSによるクラス付与方式に書き換えた。

    /* Before: animation */
    .chassis::after {
      animation: chassisAfterIn 1.8s ease 0.1s;
      animation-fill-mode: forwards;
    }
    
    @keyframes chassisAfterIn {
      100% { opacity: 0; }
    }
    
    /* After: transition + .is-loaded */
    .chassis::after {
      opacity: 1;
      transition: opacity 1.8s ease 0.1s;
    }
    
    .chassis.is-loaded::after {
      opacity: 0;
    }

    JSでページロード後に .is-loaded を付与することで、transition が発火する。

    transition は「プロパティの値が変化したとき」に発火するため、印刷エンジンのように「アニメーションを再生しない」環境でも、クラス付与時点の状態(最終値)が適用されるか、transition: none で即時反映される。


    派生:アニメーションライブラリ も書き直した

    この問題を契機に、自作のアニメーションライブラリを見直した。

    scrollin.scss

    スクロールトリガーのアニメーションライブラリ。scroll-fadeIn 等のクラスを付与すると、スクロールで要素がフェードインする。

    従来はすべて animation で実装していたが、スクロールトリガーの演出(フェードイン・ブラーイン・クリップイン等)は「一度きりの開始→終了」の2状態遷移なので、transition に置き換えられる。

    /* Before: animation */
    .scroll-fadeIn {
      opacity: 0;
      animation: none 1s ease-out 0.25s;
      animation-fill-mode: forwards;
    }
    
    .scroll-fadeIn.is-scloaded {
      animation-name: scrollFadeIn;
    }
    
    @keyframes scrollFadeIn {
      100% { opacity: 1; transform: translate(0); }
    }
    
    /* After: transition */
    :not([unscrollin]) .scroll-fadeIn {
      opacity: 0;
      transition: opacity 1s ease-out 0.25s, transform 1s ease-out 0.25s;
    }
    
    :not([unscrollin]) .scroll-fadeIn:where(.is-scloaded) {
      opacity: 1;
      transform: translate(0);
    }

    なお、animation-iteration-count: infinite を使うループ系(回転・マーキー等)は transition では再現できないため、animation のまま維持している。

    Scrollin — スクロールトリガーアニメーションライブラリ(Vanilla JS / jQuery不要) Scrollin とはscroll-fadeIn などのクラスを付けておくだけで、要素がスクロールでビューポートに入ったタイミングで自動的にアニメーションする、スクロールトリガーアニメーションライブラリです。特徴:Vanilla JS — jQuery不要In...  続きを読む

    hoverllin.scss

    ホバーアニメーションのライブラリ。こちらはもともと transition ベースだったので、animation から transition への変更は不要だった。

    ただし今回の整理のタイミングで、:not([unhoverllin]) 属性セレクタを追加し、特定のセクション内でホバーエフェクトを無効化できるようにした。

    Hoverllin — ホバーアニメーションライブラリ(CSS only / Vanilla JS不要) Hoverllin とはhvr-fadeIn などのクラスを付けておくだけで、要素にホバーしたときにアニメーションが実行される、CSSのみのホバーアニメーションライブラリです。特徴:CSS only — JavaScriptは不要transition ベース ...  続きを読む

    animation と transition の使い分けまとめ

    用途推奨理由
    一度きりの開始→終了(フェードイン等)transitionコードがシンプル、FireShot問題が起きない
    繰り返し再生(ループ・無限回転等)animationtransition では無限繰り返しが不可能
    中間フレームが必要(バウンス・フラッシュ等)animation複数の%指定が必要
    ホバーエフェクトtransition状態変化ベースなのでtransitionが自然

    FireShot が真っ白になったのは、animation が原因でした。


    関連ライブラリ