12k
All articles

z-index が効かない理由

z-indexが効かない原因をDevToolsで調査。スタッキングコンテキストを作る祖先要素を特定し、モーダル、ドロップダウン、固定ヘッダーの重なりを解決します。

OpenReplay Team
OpenReplay Team
z-index が効かない理由

z-index: 9999 を指定した要素が、2 を指定した要素の背後に描画されてしまう場合、原因は前者の祖先要素がスタッキングコンテキストを生成していることにあります。z-index は、その要素が属するコンテキストの外には決して影響しません。

すでに 99999 を試し、さらに 2147483647 も試したかもしれません。それでもモーダルはヘッダーの下に隠れたままで、Styles パネルを見ても特におかしな点は見当たりません。

重ね合わせ順序の理論については、CSS: Demystifying the z-index property を参照してください。本ガイドではデバッグに焦点を当てます。DevTools でスタッキングコンテキストを可視化する方法、要素を閉じ込めている祖先が見つかるまで DOM をさかのぼる方法、そして数値を大きくすることなく典型的なケースを解決する方法を解説します。

重要なポイント

  • z-index の値は、同じスタッキングコンテキスト内の要素同士でしか比較されません。そのため、ある要素の祖先が独自のスタッキングコンテキストを持っていると、9999 が 2 に負けることがあります。
  • 大きい z-index が負けている場合は、2 つの要素それぞれについて、共通の親を持つ祖先同士を比較します。要素自身の値では決まりません。
  • transform、filter、clip-path に none 以外の値を指定した場合や、opacity が 1 未満の場合は、position や z-index が指定されていなくても新しいスタッキングコンテキストが生成されます。
  • ドロップダウンがカードの端で切れてしまう場合、原因はたいてい overflow によるクリッピングであり、z-index の値では解決できません。
  • オーバーレイを <body> の直接の子に移動すると、すべての祖先のスタッキングコンテキストから抜け出せます。

z-index が効かない原因となる唯一のルールとは?

z-index は、同じスタッキングコンテキストに属する他の要素との間でのみ、順序を決定します。スタッキングコンテキストとは、ブラウザがひとまとまりとして描画するグループのことです。コンテキストを生成するプロパティの一覧は、MDN のスタッキングコンテキストに関するガイドに掲載されています。祖先要素がコンテキストを生成すると、子要素の z-index はその祖先の内部でしか適用されません。したがって、その祖先が別のコンテキストより下にあれば、子要素にどんな数値を指定しても、子要素もやはり下に留まります。

このバグを最小限のコードで再現すると次のようになります。

<header style="position: fixed; top: 0; z-index: 2; background: tomato;">Header</header>
<main style="opacity: 0.99;">
  <div class="modal" style="position: fixed; inset: 0; z-index: 9999; background: rgb(0 0 0 / 0.6);">
    Modal
  </div>
</main>

モーダルが負けるのは、main に opacity: 0.99 が指定されており、それによって main がスタッキングコンテキストになっているためです。main の内部では、モーダルの 9999 は正しく機能しています。しかし、ルートレベルで比較されるのは main 自体とヘッダーであり、ヘッダーの 2 のほうが大きいのです。大きい z-index が負けている場合は、祖先に目を向けてください。それぞれの要素について、もう一方の要素と共通の親を持つ最も近い祖先を見つけ、その 2 つを比較します。

DevTools でスタッキングコンテキストを確認するには?

Chromium 系の DevTools には、重なり順の問題を調査するためのビジュアルツールが用意されており、当て推量で試すよりも早く原因にたどり着けることがあります。Chrome と Edge のいずれでも、これらのビューはコマンドメニューから開けます。ショートカットは macOS では Command+Shift+P、Windows と Linux では Control+Shift+P です。あまり使われていないその他の DevTools パネルについては、Hidden Gems in Chrome DevTools を参照してください。

Chrome:Layers パネル

  1. DevTools を開き、Command+Shift+P または Control+Shift+P を押します。
  2. Layers と入力し、Show Layers panel を選択します。More options > More tools > Layers panel から開くこともできます。
  3. キャンバスを回転させ、レイヤーを選択すると、ブラウザがそのレイヤーを独立させた理由を含む詳細が表示されます。

Layers パネルでは、左側にページのレイヤーツリー、その横にレイヤーの 3D 図が表示されます。このパネルは、ブラウザがページをどのようにコンポジットレイヤーに分割しているかを調べるのに役立ちますが、コンポジットレイヤーはスタッキングコンテキストと同じものではない点に注意してください。スタッキングコンテキストの原因となっている祖先を特定するには、後述のさかのぼり手法を使います。このパネルは、カクつく CSS アニメーションのデバッグでも活用されます。

Edge:3D ビュー

  1. コマンドメニューを開き、Show 3D View を実行します。
  2. Z-index タブを選択します。
  3. Show all オプションを選択すると、スタッキングコンテキストに加えて配置済み要素(positioned element)も表示されます。

Edge の 3D ビューには、Composited Layers、Z-index、DOM の 3 つのタブがあります。Z-index タブではページのスタッキングコンテキストが 3D で描画されるため、閉じ込められた要素は下層のスラブの中に入れ子になって表示されます。

Chrome や Firefox でスタッキングコンテキストをツリー表示したい場合は、CSS Stacking Context inspector 拡張機能が便利です。コンテキストのツリーを表示するパネルに加え、選択した要素がコンテキストを生成している理由を説明する Elements サイドバーが追加されます。なお、ドキュメントによると、サードパーティの iframe 内部はまだ調査できません。

祖先さかのぼり手法

祖先さかのぼり手法は、Elements パネルと Computed ペインだけを使って、要素を閉じ込めている祖先を特定する方法です。どのブラウザでも同じように使えます。

  1. 負けている要素(モーダルやメニュー)を選択します。
  2. Elements で、その親要素をクリックします。
  3. Computed ペインを開き、後述のチェックリストにある各プロパティ(transform、opacity、filter、position、z-index など)をフィルターボックスに入力します。
  4. いずれもコンテキストを生成していなければ、1 階層上に移動して同じ作業を繰り返し、<html> に到達するまで続けます。
  5. コンテキストを生成している最初の祖先を記録します。
  6. 勝っている要素を起点に、手順 1〜5 を繰り返します。
  7. 2 つの祖先チェーンは、ある共通の祖先で合流します。その共通祖先の子のうち、それぞれのチェーン上にある 2 つの要素こそが、実際に比較されている要素です。

ローカルで再現した環境ではなくバグレポートをもとに調査している場合は、セッションリプレイを使うとユーザー側で発生した症状を確認できます。たとえば、クリックが見えないオーバーレイに吸い込まれている、メニューがコンテンツの背後に開いている、といった状況です。これにより、手順 1 でどの要素を選択すべきかがわかります。

チェックリスト:スタッキングコンテキストを生成するプロパティ

各祖先をこの表と照らし合わせてください。1 行でも該当すれば、その子孫は閉じ込められます。

プロパティコンテキストが生成される条件Computed で確認すべき点
position: absolute / relativez-index が auto 以外同じ要素に両方の値が設定されている
position: fixed / sticky常にposition の値のみ
opacity1 未満0.99 などの値。フェードアニメーションの名残であることが多い
transform、scale、rotate、translatenone 以外の任意の値matrix(...) の値(恒等変換を含む)
filter、backdrop-filternone 以外blur()、drop-shadow()
clip-path、perspectivenone 以外任意のシェイプや長さ
mask、mask-image、mask-bordernone 以外画像やグラデーションの値
will-change設定するとコンテキストを生成するプロパティが指定されているwill-change: transform または opacity
isolationisolate明示的な値
containlayout、paint、strict、content のいずれか仮想化リストでよく見られる
Flex または Grid の子要素z-index が auto 以外親要素の display: flex または grid。position は不要

MDN の完全な一覧には、このほか normal 以外の mix-blend-mode、トップレイヤーの要素、コンテキストを生成するプロパティを animation-fill-mode: forwards でアニメーションさせている要素も含まれています。MDN には現在も container-type: size と inline-size が掲載されていますが、CSS Working Group が container-type からレイアウト封じ込め(layout containment)を削除したため、現在はスタッキングコンテキストを生成しません。Chrome はバージョン 129 でこの変更を行い、その後 Firefox と Safari も追随しています。

よくある 3 つのケース:症状・原因・対処法

モーダルがヘッダーの背後に隠れる

症状: オーバーレイでページは暗くなるものの、ヘッダーだけが明るいまま上に表示される。原因: モーダルが transform、opacity、または配置済みの z-index を持つラッパー内でレンダリングされており、ヘッダーのコンテキストがそのラッパーより上位にある。対処法: モーダルを <body> に移動する(後述の対処法を参照)。

transform を持つカード内でドロップダウンが切れる

transform が適用されたカード内のドロップダウンには、スタッキングかクリッピングのいずれかの問題が起きています。まずはどちらなのかを見極めましょう。メニューが隣のカードの背後に描画されている場合は、スタッキングの問題です。transform が適用されたカードはコンテキストとなり、DOM 上で後に続くカードがその上に描画されます。開いているカードを前面に出しましょう。

.card.is-open { position: relative; z-index: 1; }

メニューがカード自身の端で切れている場合は、クリッピングの問題です。カードとその祖先の overflow を確認してください。visible 以外の値はメニューをクリップするため、z-index の値をどう変えても表示されることはありません。

スティッキー要素が最前面に留まらない

症状: ホバーやスクロールの際に、カードがスティッキーヘッダーの上に重なってしまう。原因: position: fixed または sticky を持つ要素は必ずコンテキストを生成します。しかし z-index を指定していないとレベル 0 に位置するため、コンテキストを生成する後続の兄弟要素(ホバー時に transform が適用されるカードなど)が、DOM 上で後にあるという理由でその上に描画されます。対処法:

.site-header { position: sticky; top: 0; z-index: var(--z-sticky); }

ヘッダーがまったく固定されない場合、原因はたいてい祖先の overflow です。スティッキーヘッダーの構築方法全般については、Building a responsive sticky navbar with CSS を参照してください。

閉じ込められた要素を解放する 3 つの方法

閉じ込められた要素に対する対処法は、いずれも z-index の数値ではなく、スタッキングコンテキストの構造を変えるものです。要素を移動する、プロパティを移動する、あるいは意図した場所に意図的に境界を設ける、のいずれかです。

DOM 内で要素を移動する。 <body> の直接の子であるオーバーレイには、上位に祖先のコンテキストが存在しないため、その z-index はルートで比較されます。ページ全体を覆う必要があるオーバーレイに、ポータルが非常に有効なのはこのためです。

const modal = document.querySelector('.modal');

function openModal() {
  document.body.appendChild(modal); // escape every ancestor context
  modal.hidden = false;
}

プロパティを削除または移動する。 Lullabot の Andy Blum 氏は、右から左(RTL)のレイアウトで背景動画のコンテナを transform で反転させたところ、動画の再生/一時停止ボタンが埋もれてしまったケースを紹介しています。氏は transform をコンテナから動画の iframe に移すことで、この問題を解決しました。

/* Before: the wrapper becomes a context */
[dir="rtl"] .hero { transform: scaleX(-1); }

/* After: only the media is mirrored */
[dir="rtl"] .hero iframe { transform: scaleX(-1); }

意図的に isolation を追加する。 コンポーネントのルートに isolation: isolate を設定すると、意図的にコンテキストが生成され、コンポーネント内のすべての z-index がローカルに留まります。

.card { isolation: isolate; }
.card .badge { position: absolute; z-index: 1; } /* never competes with the site header */

z-index スケールで問題を未然に防ぐ

z-index が 9999 になっているのは、ほとんどの場合、より大きな数値が必要だったからではなく、誰かがスタッキングコンテキストを見つけられなかったからです。場当たり的な値は、小規模で文書化されたスケールに置き換えましょう。

:root {
  --z-base: 0;
  --z-dropdown: 100;
  --z-sticky: 200;
  --z-overlay: 300;
  --z-modal: 400;
  --z-toast: 500;
}
.modal { z-index: var(--z-modal); }

z-index スケールのトークンが順序を決められるのは、同じスタッキングコンテキストを共有する要素同士だけです。そのため、ページレベルのレイヤーにはポータルを、コンポーネントのルートには isolation: isolate を併用すると最も効果的です。

まとめ

z-index が効かない問題は、ほぼ例外なく数値の問題ではなく、スタッキングコンテキストの問題です。次に 9999 が負けたときは、値には手を付けないでください。Layers パネルや 3D ビューを開いて何がどこに描画されているかを確認し、チェックリストを使って両方の要素から祖先をさかのぼり、2 つのチェーンが合流する地点を見つけます。共通祖先の 2 つの子が比較対象となっている要素であり、修正すべきはそのどちらかです。

よくある質問

要素に z-index がまったく効かないのはなぜですか?

z-index が適用されるのは、配置済み要素(position に relative、absolute、fixed、sticky のいずれかが設定された要素)と、flex アイテムまたは grid アイテムだけです。デフォルトの position: static を持ち、flex や grid の子でもない要素では、ブラウザは z-index を完全に無視します。position: relative を追加して値を有効にしたうえで、祖先のスタッキングコンテキストが依然として制約になっていないかを確認してください。

showModal() で開いたダイアログはスタッキングコンテキストから抜け出せますか?

はい。dialog 要素で showModal() を呼び出すと、その要素はトップレイヤーに昇格します。トップレイヤーはドキュメント内のすべてのスタッキングコンテキストより上に位置するため、祖先の transform や z-index の値は順序に影響しなくなります。showPopover() で表示したポップオーバーも同様に昇格します。トップレイヤー内では z-index は効果を持たず、要素は昇格した順に積み重なるため、最後に開いたものが最前面に表示されます。

position: fixed のオーバーレイが、ビューポートではなく親要素を基準に配置されるのはなぜですか?

transform、perspective、filter に none 以外の値を持つ祖先は、固定配置された子孫の包含ブロックになります。その内部にある固定オーバーレイは、ビューポートではなくその祖先を基準に配置・サイズ決定されます。同じプロパティはスタッキングコンテキストも生成するため、オーバーレイが他のコンテンツの背後に隠れると同時に、誤った領域を覆ってしまうこともあります。オーバーレイを body に移動すれば、両方の問題が解決します。

負の z-index で子要素を親要素の背後に配置できますか?

親要素がスタッキングコンテキストを生成していない場合に限り可能です。スタッキングコンテキスト内では、ブラウザはまずコンテキストのルート自身の背景とボーダーを描画し、次に負の z-index を持つ子要素、その後にインフローのコンテンツを描画します。親要素の opacity が 1 未満である場合、transform を持つ場合、あるいは配置済みで auto 以外の z-index を持つ場合は、z-index: -1 の子要素であっても、親要素の通常のコンテンツより下ではあるものの、親要素の背景より上に描画されます。

Open-source session replay

Gain control over your UX

See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.