Web エンジニアなら知っておきたい重ね合わせコンテキスト

こんにちは。フロントエンド担当の水野です。
「Web エンジニアなら知っておきたい」シリーズということで、今回は「重ね合わせコンテキスト」についてのお話をしていこうと思います。
今までのシリーズ記事と比べるとかなりニッチなところを扱うことになりますが、Web フロントエンド開発に避けては通れないと考えているので、お付き合いいただければと思います。

重ね合わせコンテキストとは?

みなさまは CSS の z-index プロパティで HTML 要素の重なりをコントロールしようとした経験はありますか?
ダイアログやポップアップなど、他の要素よりも手前に表示させたい UI を実装するために必要なプロパティですが、思ったように重なってくれないと感じる方は多いのではないでしょうか。
この「うまくいかない」ケースに関係してくるのが、重ね合わせコンテキストです。

重ね合わせコンテキストは、簡単に言えばブラウザが要素同士の重なりを処理するための概念です。 ある重ね合わせコンテキストは別の重ね合わせコンテキストを内部に含めることができ、DOM のように階層構造を形成しています。

重ね合わせコンテキストを体験する

z-index の素朴な理解としては「0 を基準として大きいほど手前に表示される」というところでしょうか。
実際、未指定の要素は z-index: 0; と同じレイヤーに描画されるため、単純なケースではこの理解で問題になることはないでしょう。
しかし、複数の要素の重なりをコントロールしようとすると、重ね合わせコンテキストのためにこの素朴な理解とは反する挙動をするケースがあります。

下記のようなレイアウトを考えましょう。

HTML

<div id="container">
  container div
  
  <div class="box-1">
    box-1
    <div class="box-2">box-2</div>
  </div>
  <div class="box-3">box-3</div>
  
</div>

CSS

div {
  outline: dashed #aaa 1px;
  position: relative;
}
#container {
  position: relative;
  background: #FFFFFFAA;
  width: 300px;
  height: 250px;
}

.box-1 {
  width: 100px;
  height: 100px;
  background: #FFDDDDCC;
  margin-left: 10px;
}

.box-2 {
  width: 100px;
  height: 100px;
  background: #DDFFDDCC;
  margin-top: 5px;
  margin-left: 25px;
}

.box-3 {
  width: 100px;
  height: 100px;
  background: #DDDDFFCC;
  margin-left: 60px;
  margin-top: -45px;
}

一つの大きなコンテナー要素の中に、box-1 ~ box-3 までナンバリングされた div 要素がレイアウトされています。また、box-2box-1 の子要素として配置されており、それぞれ一部分が重なるようなレイアウトになっています。
これらは同じレイヤー上(z-index: 0;)に配置されているため、HTML の規定動作の通り、親要素(box-1)より子要素(box-2)、先に配置された要素(box-1, box-2)より後に配置された要素(box-3)が手前に表示されます。 (また、position: relative; をすべての div 要素に適用していますが、これは z-index 指定を有効にするための準備です。)

ここで、各 box に下記のように z-index の指定を与えます。

  • box-1z-index: 3;
  • box-2z-index: 1;
  • box-3z-index: 2;

さて、box-1 ~ box-3 の重なりの順番はどうなるでしょうか?
先の素朴な理解を適用すると、画面の奥側から box-2box-3box-1 の順番でレイアウトされるはずです。

ところが、実際には画面の奥側から box-3box-1box-2 の順番でレイアウトされます。
少し直感に反する結果だったでしょうか。

この結果になるメカニズムを説明しましょう。
それぞれの box に対してz-index の指定がされたことにより、box はそれぞれ重ね合わせコンテキストを発生させます。
重ね合わせコンテキストに属する要素の配置順は同じコンテキスト上にあるもの同士でしか比較できず、重ね合わせコンテキストに属する要素の子要素は親要素が属する重ね合わせコンテキストに属するものとして扱われます。
少し分かりづらいので整理してみましょう。

  • box-1 の重ね合わせコンテキスト(z-index: 3;)
    • box-1 の背景・テキスト
    • box-2 の重ね合わせコンテキスト(z-index: 1;)
      • box-2 の背景・テキスト
  • box-3 の重ね合わせコンテキスト(z-index: 2;)
    • box-3 の背景・テキスト

box-1box-2 の表示順は、box-1 の重ね合わせコンテキストの中で box-2z-index: 1; を持っているため box-2 が手前に配置されます。
box-1box-3 の表示順は、box-1box-3 が兄弟関係にある要素なので、単純により大きな z-index の指定された box-1 が手前に配置されます。
box-2box-3 の表示順は、box-2box-1 の重ね合わせコンテキストに属するものとして扱うので、box-1box-3 の表示順に従い box-2 が手前に配置されます。
結果、画面の奥側から box-3box-1box-2 の順に配置される、というわけです。

また、重ね合わせコンテキストが発生する条件には position: relative; 及び position: absolute; が指定された要素への z-index の指定のほか、position: fixed;position: sticky; の指定なども該当します。
込み入った話になるので本稿ではそのすべてを列挙しませんが、意外なプロパティの組み合わせで重ね合わせコンテキストが発生することもあるので注意が必要です。

まとめ

重ね合わせコンテキストの概念を理解していないと、モーダルダイアログの実装などの際にバックドロップより前に表示されてしまう不具合を作り込んでしまいがちです。
状態管理ライブラリや React の Portal 機能を活用して、できる限りルートに近い要素の子要素として作成しておくと表示不具合を起こしにくくなります。
アプリケーションの UI 実装全体の戦略立てにも関わってくるため、Web フロントエンド開発者は重ね合わせコンテキストの仕組みを把握しておきたいところです。