‘疑似要素’+’animation’+’display: none;’ で条件が重なると Chrome(115以降)がおかしくなる(たぶんバグなんじゃないかな~)

‘疑似要素’+’animation’+’display: none;’ で条件が重なると Chrome(115以降)がおかしくなる(たぶんバグなんじゃないかな~)

バージョン 121でも解消されず(2024年01月25日現在)

  • macOS Monterey 12.7.2 + Chrome 121.0.6167.85
  • Windows 10 Pro 22H2 + Chrome 121.0.6167.86

バージョン 120でも解消されず(2023年12月14日現在)

  • macOS Monterey 12.7.1 + Chrome 120.0.6099.109
  • Windows 10 Pro 22H2 + Chrome 120.0.6099.110

バージョン 119でも解消されず(2023年11月22日現在)

  • macOS Monterey 12.7.1 + Chrome 119.0.6045.159
  • Windows 10 Pro 22H2 + Chrome 119.0.6045.160

バージョン 118でも解消されず(2023年10月12日現在)

  • macOS Monterey 12.6.8 + Chrome 118.0.5993.70
  • Windows 10 Pro 22H2 + Chrome 118.0.5993.71

バージョンアップ(114 → 115)でループに突入

《検証環境》

  • 問題あり
    • macOS Monterey 12.6.8 + Chrome 117.0.5938.62
    • Windows 10 Pro 22H2 + Chrome 117.0.5938.63
  • 問題なし
    • macOS Monterey 12.6.8 + Safari 16.6
    • Windows 10 Pro 22H2 + Firefox 117.0.1

ページの読み込み時に表示するローディングアニメーションを疑似要素で作成したら、Safariでは確認できないおかしな現象に遭遇。ちなみに、Chromeも 114までは問題なく、115以降から発生している。

事の発端は7月下旬のある日──。
ローディング画面がループしてページがきちんと表示されないという話があり、こちらの Chromeでは問題ないなぁ~というやりとりを行っている最中、Chromeにバージョンアップの表示がでたため、そのまま指示どおりに Chromeを再起動しバージョンアップ(114 → 115)したところ、こちらの Chromeでもローディング画面がループしだした。

急遽、ローディングの設定を外し、他にもローディング画面を導入しているサイトをチェックすると、ローディングのギミックを疑似要素で生成しているページで同様の現象が起きていることを確認。

但し、疑似要素を使っているサイトで必ず起きているわけではないことも同時に判明し、その原因究明のため色々と検証することに。

キーフレーム 100%の状態が保持されない

まず結論から述べると、以下の組み合わせで CSSアニメーションがうまく機能しなくなる場合がある。そして、これらの条件が揃うと animation-iteration-count: 1; と指定してもアニメーションはループする。

  1. 同じ要素内の 2つの疑似要素「::before」と「::after」に対して、ローディングのCSSアニメーションを設定している
  2. アニメーションのキーフレーム 100%のタイミングで、双方に「display: none;」を設定している
  3. 2つの疑似要素に設定した animation-duration の時間が違う

同じ要素内の 2つの疑似要素にCSSアニメーションの設定を行うとは、<div class="container"></div> 要素内の疑似要素.container::before.container::after に対して設定を行うということ。

実際にローディング画面のループが起こったサイトでは、::before にだけアニメーションを設定し、::after には何の設定も行ってはいなかったが(苦笑)、<div class="container"></div> の子・孫要素の divタグに背景を動かすためのアニメーション(animation-iteration-count: infinite; を設定)や、ページをスクロールすると各要素がふわっと表示されるアニメーションなどを設定しており、それらが複雑に絡み合って起きたようだ。

しかしながら、問題の起きたページでは要素をひとつずつ削除したり、各アニメーション設定を切ったりしても、これが原因だ!といったものにはたどり着くことができず行き詰まる。

その後しばらく放置していたが、ふと、同じ要素内の 2つの疑似要素それぞれにアニメーションの設定を行ってはどうかと考え実行してみると、同様の現象が簡単に再現できることが分かったため、記事を作成することに。

Chrome 117でも引き続きおかしい(2023-09-15現在)

というわけで、問題の挙動を再現するページを作ってみた。

基本的は外部の style.cssで設定し、この件で重要な CSSについては、htmlファイルの head内に記述しているので、その部分だけを抜粋。

  <style>
    .container::before,
    .container::after {
      content: "";
      display: block;
      position: fixed;
    }
    .container::before {
      background-color: rgba(255, 255, 255, 1);
      width: 100%;
      height: 100%;
      top: 0;
      left: 0;
      z-index: 93;
      opacity: 1;
      animation: loading-screen 2.33s ease forwards;
    }
    @keyframes loading-screen {
      0% {
        z-index: 93;
        opacity: 1;
      }
      75% {
        z-index: 93;
        opacity: 1;
      }
      99.9% {
        z-index: 93;
        opacity: 0;
      }
      100% {
        display: none;
        z-index: -93;
        opacity: 0;
      }
    }
    .container::after {
      background-color: transparent;
      border: .2em solid #535353;
      border-top-color: #ff7878;
      border-radius: 50%;
      width: 2.5em;
      height: 2.5em;
      margin: auto;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      z-index: 95;
      opacity: 1;
      animation: loading-icon 2s linear forwards;
    }
    @keyframes loading-icon {
      0% {
        z-index: 95;
        opacity: 1;
        transform: rotate(0);
      }
      50% {
        z-index: 95;
        opacity: 1;
        transform: rotate(360deg);
      }
      67% {
        z-index: 95;
        opacity: 1;
      }
      99.9% {
        z-index: 95;
        opacity: 0;
      }
      100% {
        display: none;
        z-index: -95;
        opacity: 0;
        transform: rotate(720deg);
      }
    }

  </style>

コードの詳細については、各サンプルの htmlファイルを直接見て欲しい。

  • .container::before … ブラウザウインドウの全面を覆う白い矩形を生成
  • .container::after … ページの読み込みを示すアイコンを生成

Chromeがおかしくなる設定:サンプルページ (1)

CSSアニメーションがループする基本セット。
example_1.html
バージョン115以降の Chrome(2023-09-15現在のバージョン117も含む)だとローディング画面がループするので注意!

以下の条件がポイント

  • 2つの疑似要素の両方に、キーフレーム 100%のタイミングで display: none; を設定
  • .container::beforeanimation-duration を「2.33秒」に設定
  • .container::afteranimation-duration を「2秒」に設定

なお、animation-duration の時間設定をいじるとループの回数が増減する。10分放置してもループしている場合もあれば、10回程度ループしたら終了する場合もある。どうやら、それぞれのアニメーションの終了のタイミングが合うとループも終了するようだ。

時間をずらしている理由は、白い矩形とローディングアイコンのアニメーションが終了する時間を揃えてしまうと、目の錯覚でアイコンの方が後から消えてるように見えるため、手っ取り早く animation-duration でアニメーションの終了のタイミングを調整したのだが、それが良くなかったらしい。

というわけで、時間設定は揃えて opacity: 1;opacity: 0; のタイミングをキーフレーム側で調整するとループしなくなる。

キーフレーム側で表示時間を調整したファイル

まぁ、そもそも display: none; を設定しなければ問題ないんだけどね。

アニメーションが終了した後、白い矩形やアイコンは透明にしているし、重なり順でもかなり下の層にブッ飛ばしているので、それで良しとしていればね。でも、要素自体は存在はしているので、トドメとばかりに display: none; で存在を消したら裏目に。

いやいや、Safariや Firefox、もしくはバージョン 114までの Chromeではこんな現象は起きなかったのだから、やっぱり Chromeのバージョン 115以降のバグでしょう。

CSSの設定を1箇所だけ変更:サンプルページ (2)

このサンプルは、example_1.htmlの CSSの設定を1箇所だけ変更。
example_2.html

.container::beforedisplay: none; をコメントアウト

  • .container::before のキーフレーム 100%のところで設定している display: none; をコメントアウトし無効化

これにより、.container::beforeの白い矩形はループしなくなった。しかし、.container::afterのアイコンはアニメーションが2回実行される。ループする回数は減ったけどね。

別の display: none; をコメントアウトする:サンプルページ (3)

このサンプルは、example_2.htmlとは逆に .container::after を変更。
example_3.html

.container::afterdisplay: none; をコメントアウト

  • .container::before のキーフレーム 100%の display: none; は有効のまま
  • .container::after のキーフレーム 100%の display: none; を無効に

このケースでは、ループは発生しない。

この結果から、CSSの読み込みの順番が関係しているのかと考え example_2.html、example_3.htmlの .container::before.container::after の記述位置を入れ替えてみたが結果は変わらず。

疑似要素の記述位置を入れ替え

それならばと、.container::before.container::after の設定自体を入れ替えてみると。

疑似要素の設定を入れ替え

…example_2.html、example_3.html共に挙動は変わらず。

疑似要素の片方を無限に回転するアニメーションにしてみる:サンプルページ (4)

このサンプルページでは、アイコンだった .container::after を矩形に変更し、ページの背景に表示されるようにした。
example_infinite.html
バージョン115以降の Chrome(2023-09-15現在のバージョン117も含む)だとローディング画面がループするので注意!

animation-iteration-count: infinite; を設定

  • .container::before のキーフレーム 100%の display: none; は有効のまま
  • .container::after をアイコンから矩形に変更して、無限に回転させるため animation-iteration-count: infinite; を設定

このケースでは(予想どおり)アニメーションの読み込みがループする。

色々と試した結果、なぜ example_3.html(と、その派生ページ)ではループしないのか分からないが、大まかな状況は理解できた感じ。

疑似要素だけでローディング画面を作らない

以上のことを踏まえて、疑似要素オンリーではなく疑似要素と空の divタグの組み合わせはどうなるのか気になったので、それを試したのが以下のページ。

こちらは、サンプルページ (1) / example_1.html.container::before に設定していたCSSアニメーションを空の divタグに再設定したもの。
example_loading_1.html

白い矩形を空の divタグで生成

  • <div class="loading"></div> … ブラウザウインドウの全面を覆う白い矩形を生成
  • .container::after … ページの読み込みを示すアイコンを生成

こちらは、サンプルページ (4) / example_infinite.html.container::before に設定していたCSSアニメーションを空の divタグに再設定し、無限回転は .container::after に設定したまま。
example_loading_2.html

無限回転の設定があっても大丈夫

  • <div class="loading"></div> … ブラウザウインドウの全面を覆う白い矩形を生成
  • .container::after … レインボウカラーの矩形を生成し背景に配置し無限回転させる

結果、これら2つのページは問題なし。こちらが意図したとおりの挙動を示す。

以上、もっと調べれば example_3.html系統でなぜループしないのか分かるのかもしれないが、回避策が分かった時点で自分的にはOKとなっている。もしかしたら、これが Chrome 115からのバグではなく仕様なのかもしれないし…。