CSSで要素の重なりを表現する時はスタッキングコンテキストによって決められています。スタッキングコンテキスト(Stacking Context)はウェブページ上の仮想的な奥・手前方向の概念であり、「重ね合わせコンテキスト」、あるいは「スタック文脈」とも言います。
z-indexによる重なり位置の指定もこのスタッキングコンテキストのうちの1つです。今回はz-indexより広い概念のスタッキングコンテキストの深淵を覗いてみます。
z-index:5がz-index:53万に勝つ方法
重なりといえば、z-indexです。z-indexはWeb初心者キラーなプロパティで、その値が必ずしも重なりの順序になりません。たとえば次のようなz-indexが53万と5の要素があったとします。この場合、53万の要素が上にきます。
<div class="wrapper wrapper-freeza">
<div class="freeza">私のz-indexは53万です</div>
</div>
<div class="wrapper wrapper-mob">
<div class="mob">z-indexたったの5</div>
</div>
.wrapper {
position:relative;
}
.freeza {
position:absolute;
z-index: 530000;
}
.mob {
position:absolute;
z-index: 5;
}

しかし、次のように親要素にz-index: 1を追加すればz-indexが5の要素が上にきます。
.wrapper-freeza {
z-index: 1;
}

これはz-index: 1の中にz-index: 530000が内包されることで、z-index: 5と比較するのがz-index: 1に変わるからです。なので、z-indexが5の要素が53万の要素の上にきます。
初心者向けの説明であればこれで済み、普段の業務でも上記のようなコードで問題ないでしょう。今回はどんなルールで重なりの上下関係が決定しているか、もう少し深堀りしてみます。
スタッキングコンテキストの生成
さきほど、「内包される」と表現しましたが、親要素にスタッキングコンテキストが生成されるという表現がより正確です。Webページの重なり関係は基本的にこのスタッキングコンテキストのルールに基づいて決定されています。
まず、z-indexで重なりを比較するのは同一スタック上の要素です。先の例では、.wrapper-freezaにz-index:1が指定されたことでスタッキングコンテキストが生成され、53万の要素は別のスタックの中へ移動したわけです。
z-indexの指定だけでなく、スタッキングコンテキストを生成するプロパティを追加すれば解決できます。意外かもしれませんが、次のプロパティでもスタッキングコンテキストは生成されます。
.wrapper-freeza {
opacity: 0.99;
}

主だったスタッキングコンテキストを生成するプロパティは以下の通りです。
positionの値がabsoluteあるいはrelativeで、かつz-indexの値がauto以外positionの値がfixedあるいはstickydisplay: flexあるいはdisplay: gridの子要素で、かつz-indexの値がauto以外opacityの値が1以外mix-blend-modeの値がnormal以外transform、filter、perspective、clip-path、mask、mask-image、mask-borderの値がnone以外isolationの値がisolateの場合will-changeの値がauto以外で上記のようなプロパティを指定している場合
ここで注意したいのは、positionでも値がabsolute(relative)とfixed (sticky)で扱いが違う点です。position: absoluteがz-indexの値を指定しないとスタッキングコンテキストが生成されないのに対し、position: fixedは指定した時点でスタッキングコンテキストが生成されます。
ほかにも、display: flex、display: gridで並べている子要素もz-indexを指定することで重ね合わせコンテキストが発生します。あまりないかもしれませんが、display: girdで並べた要素の重なり順を指定したい時に使えるテクニックです。
floatとの関係
上下の重なりと言えば、floatプロパティも忘れてはいけません。display: flexが出るまで主流の横並びプロパティでした。floatはその文字の通り、浮いた要素になります。ただし、スタッキングコンテキストとは別ものです。
上下の関係はどのようになるかというと、スタッキングコンテキストより下のレイヤーになります(何もしていない要素とスタッキングコンテキスト要素の間)。

スタッキングコンテキスト同士の高低
では同一スタック上のスタッキングコンテキスト同士の高低はどのように決まるのでしょうか?次の優先度で決まっていきます。
z-indexの値(有効なプロパティが設定されている時のみ)- 要素の出現順
z-indexの値が1以上あるもの同士はその値の大きい方が上になります。z-indexの値があるものとないものとを比べた時はz-indexのあるものが上になります。

z-indexがないもの同士、z-indexの値が0の場合、あるいは同じ値の場合は、要素の出現があとに出てきた要素が上になります。

高さ関係で言えば、
z-index:0 = z-index: auto = z-indexのないプロパティ
という関係が成り立ちます。

出現順は基本的にはHTMLのコード順になりますが、flexのorderプロパティなどで順番を入れ替えた場合はそのorder順になります。
また、z-indexにマイナスの値を使うと、重なり方は背景方向になります。z-indexが53万の例と同様、同一スタック上の背景方向の位置であって、別スタックや親要素より後ろに配置されるとも限りません。
たとえば、.hogeにスタッキングコンテキストを生成させた場合、その中の要素は<body>より後ろには配置されません。
<body>
<div class="hoge">
<div class="background"></div>
</div>
</body>
.hoge {
position: absolute;
z-index: 1;
}
.background {
position: fixed;
z-index: -1;
}
.hogeにスタッキングコンテキストが生成されているので、.backgroundのz-indexの値は.hogeの中のスタッキングコンテキストの相対的位置を示します。これは一番最初の例の負方向バージョンといえる話です。

positionの落とし穴
position: absolute (relative)はz-indexの値がautoでない場合にスタッキングコンテキストが生成される、と書きましたが、z-indexが無指定(デフォルトのz-index: auto)でも、スタッキングコンテキストのような重なりが発生します。

つまり、position: absolute (relative)を指定した時点で通常の要素より上にきて、z-index: autoの振る舞いをします。そのため、transformなどのスタッキングコンテキストの後にposition要素を配置した場合、スタッキングコンテキストでもないのにもかかわらず、スタッキングコンテキストより上になります。ただし、z-index: 0の時とは違いスタッキングコンテキストは生成されないので、子要素のz-indexは親要素のスタックで影響します(53万の例の初期状態と同じです)。
また、z-index: autoの要素とz-index: 0の要素が並んだ場合、要素の出現順に応じて上下関係が決定します。

予期せぬ重なり
ここまでの挙動は、なんとなくpositionを使うと上にくるんだなー、という認識でもz-indexの値と関係さえ気をつけていればそこまで問題になりません。しかし、他のスタッキングコンテキストとの複合では予期せぬ重なりが発生する可能性があります。
親より上の要素よりも、さらに上にいく
z-index: autoの要素の中にz-index: 1の要素があった場合、要素の出現順によっては、
z-index: auto < スタッキングコンテキスト < z-index: 1
という関係性が成り立ちます。この時、親要素より上にある、スタッキングコンテキストを子要素が上回るという、現象が発生します。

transitionで急に発生する
ほかにも注意したいのは、opacityでもスタッキングコンテキストが発生することです。とくに1以外の値で発生するところが予期せぬ重なりを発生させるかもしれません。たとえば次のような場合です。
.hoge:hover {
opacity:0.5;
}
このとき、非ホバー時はopacity: 1なのでスタッキングコンテキストは生成されず、ホバーした時のみにスタッキングコンテキストが生成されます。
より具体的には下記デモのようなカードデザインでアイコンを載せていた場合に、opacityをかける場所を間違えると、予期せぬ重なりが発生します。
これは、アイコンの要素とopacityの要素が並列になっており、スタッキングコンテキストが発生すると、要素順に応じてopacityが上になってしまうためです。

z-indexより強い、最上位レイヤー
z-indexの値やスタッキングコンテキストにとらわれず、常に最上位に表示する「最上位レイヤー(Top layer)」という概念があります。これを持つ要素はz-indexの値やスタッキングコンテキストに関係なく、常に最上位に表示されます。
具体的には以下の3つがあります。
- 全画面要素
<dialog>要素- ポップオーバー要素
全画面要素は動画などを全画面モードにしたときのものなので、本記事の内容とはあまり関係ないので省略します。
<dialog>要素は表示切り替え機能を持つ要素です。モーダルウィンドウやアラート表示を実装するときに便利な要素で、これらは多くの場合画面の最上位に出てきてほしいものです。<dialog>要素を使えば、z-indexを気にせず実装できます。詳しい実装方法などは「HTMLでモーダルUIを作るときに気をつけたいこと』内にて解説しています。
ポップオーバー要素は<dialog>要素と似ていますが、特定の要素ではなく任意の要素に対してポップオーバー機能を付与できます。また、<dialog>要素と違いポップオーバーが表示されている間も他の操作が可能です。ツールチップやトーストのようなものを実装するときに便利です。詳しくは『階層メニューやトーストUIが簡単に作れる新技術! JavaScriptで利用するポップオーバーAPI』や『ツールチップの実装に役立つ! HTMLの新属性popover="hint"の使い方』をご覧ください。
いずれも今まではz-indexやスタッキングコンテキストに気をつけながら実装していたので、気にせず常に最上位で表示されるのは嬉しいですね。
俺たちは雰囲気でz-indexをやっている
position、z-indexプロパティと重ね合わせコンテキスト関係性はきちんと見てみると結構複雑です。私もz-indexの重なりについては冒頭の「包含関係」くらいの認識でした。
絶対的な指針ではありませんが、以下のことを気をつけてコーディングするとz-indexまわりの不具合回避に役立つでしょう。
z-indexのスコープを気をつける- とくに
transform、opacityなどとpositionを組み合わせるときは気をつける - 重なりの親要素に
z-index: 1などを指定してスコープを閉じてしまう - ヘッダーやポップアップ背景など一番上に表示したい要素はルートの直下でコードの後の方(
</body>の直前など)に配置する
正直、z-indexやスタッキングコンテキストの一挙一動について理解していなくても一番最初の例の仕組みさえ分かっていれば実務上はそんなに困りません。「あれ、重なりがおかしいな?」と思ったら、この記事のことを思い出してもらえれば幸いです。
参考文献
※この記事が公開されたのは5年前ですが、5か月前の5月に内容をメンテナンスしています。

