君は真に理解しているか?
z-indexとスタッキングコンテキストの関係

104
81
87

CSSで要素の重なりを表現する時はスタッキングコンテキストによって決められています。スタッキングコンテキスト(Stacking Context)はウェブページ上の仮想的な奥・手前方向の概念であり、「重ね合わせコンテキスト」、あるいは「スタック文脈」とも言います。

z-indexによる重なり位置の指定もこのスタッキングコンテキストのうちの一つです。今回は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-freezaz-index:1が指定されたことでスタッキングコンテキストが生成され、53万の要素は別のスタックの中へ移動したわけです。

z-indexの指定だけでなく、スタッキングコンテキストを生成するプロパティを追加すれば解決できます。意外かもしれませんが、次のプロパティでもスタッキングコンテキストは生成されます。

.wrapper-freeza {
  opacity: 0.99;
}

主だったスタッキングコンテキストを生成するプロパティは以下の通りです。

  • positionの値がabsoluteあるいはrelativeで、かつz-indexの値がauto以外
  • position の値が fixed あるいは sticky
  • display: flexあるいはdisplay: gridの子要素で、かつz-indexの値がauto以外
  • opacityの値が1以外
  • mix-blend-modeの値がnormal以外
  • transformfilterperspectiveclip-pathmaskmask-imagemask-borderの値がnone以外
  • will-changeの値がauto以外で上記のようなプロパティを指定している場合

ここで注意したいのは、positionでも値がabsolute(relative)fixed (sticky)で扱いが違う点です。position: absolutez-indexの値を指定しないとスタッキングコンテキストが生成されないのに対し、position: fixedは指定した時点でスタッキングコンテキストが生成されます。

ほかにも、display: flexdisplay: gridで並べている子要素もz-indexを指定することで重ね合わせコンテキストが発生します。あまりないかもしれませんが、display: girdで並べた要素の重なり順を指定したい時に使えるテクニックです。

floatとの関係

上下の重なりと言えば、floatプロパティも忘れてはいけません。display: flexが出るまで主流の横並びプロパティでした。floatはその文字の通り、浮いた要素になります。ただし、スタッキングコンテキストとは別ものです。

上下の関係はどのようになるかというと、スタッキングコンテキストより下のレイヤーになります(何もしていない要素とスタッキングコンテキスト要素の間)。

スタッキングコンテキスト同士の高低

では同一スタック上のスタッキングコンテキスト同士の高低はどのように決まるのでしょうか?次の優先度で決まっていきます。

  1. z-indexの値(有効なプロパティが設定されている時のみ)
  2. 要素の出現順

z-indexの値が1以上あるもの同士はその値が大きい方が上になります。z-indexの値があるものとないものとを比べた時はz-indexがあるものが上になります。

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

高さ関係で言えば、

z-index:0z-index: autoz-indexのないプロパティ

という関係が成り立ちます。

出現順は基本的にはHTMLのコード順になりますが、flexorderプロパティなどで順番を入れ替えた場合はその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にスタッキングコンテキストが生成されているので、.backgroundz-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をやっている

positionz-indexプロパティと重ね合わせコンテキスト関係性はきちんと見てみると結構複雑です。私もz-indexの重なりについては冒頭の「包含関係」くらいの認識でした。

絶対的な指針ではありませんが、以下のことを気をつけてコーディングするとz-indexまわりの不具合回避に役立つでしょう。

  • z-indexのスコープを気をつける
  • 特にtransformopacityなどとpositionを組み合わせるときは気をつける
  • 重なりの親要素にz-index: 1などを指定してスコープを閉じてしまう
  • ヘッダーやポップアップ背景など一番上に表示したい要素はルートの直下でコードの後の方(</body>の直前など)に配置する

正直、z-indexやスタッキングコンテキストの一挙一動について理解していなくても一番最初の例の仕組みさえ分かっていれば実務上はそんなに困りません。「あれ、重なりがおかしいな?」と思ったら、この記事のことを思い出してもらえれば幸いです。

参考文献