「モーダルが背景の下に潜ってしまう」「ドロップダウンがヘッダーより後ろに隠れる」──フロントエンド実装で頻出する“最前面に出したいのに出ない”問題の多くは、z-index と “スタッキングコンテキスト(stacking context)” を正しく理解していないことが原因です。この記事では、CSSで要素を確実に“最前面”に表示するための理屈と実践テクニックを丁寧に解説します。z-index が効かない典型パターン、スタッキングコンテキストを生むトリガー、現場で役立つ設計指針、デバッグのコツまで一気に整理しましょう。
CSSで「最前面」に表示する基本:z-indexとは?
z-index は、同一のスタッキングコンテキスト内での「重なり順(奥行き)」を数値で指定するプロパティです。数値が大きいほど前面に描画されます。
.modal {
position: fixed; /* positionがstaticのままだとz-indexは効かない */
z-index: 9999;
}
ポイントは以下の2つです。
z-indexが効くのは、positionがstatic以外(relative,absolute,fixed,sticky)のとき。- 比較されるのは同じスタッキングコンテキスト内の要素同士であること。
この2番目の「スタッキングコンテキスト」が、混乱の元になります。
なぜz-indexが効かない?最大の犯人「スタッキングコンテキスト」
スタッキングコンテキスト(以下、SC)は「重なり順の判定グループ」のようなものです。異なるSC同士では、いくら z-index を大きくしても越えられないケースがあります。
代表的に“新しいSC”を作る条件(抜粋)
positionがabsolute/relative/fixed/stickyで、z-indexに数値(auto以外)を指定した要素opacityが1より小さい要素(例:opacity: 0.9999でもアウト)transformが指定された要素(transform: translate(0, 0);など、値がnoneでない)filter,perspective,mix-blend-mode,isolation: isolatewill-changeでtransformなどを指定contain(特にcontain: paint)position: fixedの要素(ルートに対してSCを作る)
このような親要素がSCを作ると、その内側の要素は外側の別SCに属する要素の前面には出られません。
よくある「前に出ない」実例と解決策
1. モーダルがヘッダーより後ろに隠れる
<header class="header">...</header>
<div class="modal">...</div>
.header {
position: relative;
z-index: 1000;
transform: translateZ(0); /* これが新しいSCを作ってしまう */
}
.modal {
position: fixed;
z-index: 9999; /* でもheaderの外の別SCなので、勝てないことがある */
}
解決策
- 可能ならSCを作っている原因(例:
transform)を外す。 - それが無理なら、モーダルを
body直下に配置し、最上位レイヤーで扱う(ポータル/teleport戦略)。 - “z-indexスケール”をプロジェクト全体で決め、それを越える“レイヤー”はDOMルート直下に置く。
2. ドロップダウンが親カード(transform付き)に隠れる
親に transform が付いていると、その子はSCに閉じ込められます。ドロップダウンをポータルでDOM上位に描画するか、親の transform を回避する(transform を使わずにGPUアクセラレーションを切るなど)必要があります。
大規模プロジェクトでのz-index設計指針
1. レイヤースケールを決める
よく使われる例:
:root {
--z-base: 0; /* 通常要素 */
--z-dropdown: 1000; /* ドロップダウン、ツールチップ */
--z-sticky: 1100; /* 固定ヘッダーなど */
--z-modal: 2000; /* モーダル */
--z-toast: 3000; /* トースト通知 */
--z-devtools: 999999; /* デバッグ用など */
}
各コンポーネントはこのトークンを参照して統一する。
.modal {
position: fixed;
z-index: var(--z-modal);
}
2. SCを安易に作らない
transform: translateZ(0) や opacity: 0.999 といった“なんとなく最適化”でSCを増やすと、可視化の混乱が起きます。必要な箇所だけに絞る運用を。
3. レイヤーをDOM階層で分ける
React/ Vue などでは、ポータル(createPortal, Teleport)を使い、<body> 直下にモーダルやドロップダウンを出すのが定番。これによりSCのネスト問題を避けられます。
positionの違いと「前面」への影響
position: static:z-index無効(指定しても効かない)。position: relative / absolute / fixed / sticky:z-index有効。position: fixed:ビューポート基準になるため、他要素と被りやすく、しばしば最前面として機能する。ただし、親のSCに縛られるケースもある点に注意。
CSSレイヤー化の新潮流:@layer と z-index は別物
CSS Cascade Layers(@layer)は「カスケード優先順位(どのスタイルが勝つか)」を制御する仕組みで、描画の重なり順(z軸)とは無関係です。@layer で宣言順を管理しても、要素の最前面/背面は変えられない点を混同しないようにしましょう。
デバッグのコツ:DevToolsで“どのSCに属しているか”を見る
- Chrome DevToolsで要素を選択
- Computed タブで
z-indexを確認 z-indexがautoになっていないか、positionがstaticではないかを確認- 親要素を辿って、
transformやopacity < 1などSCを作っていないかチェック - 必要であれば
position: relativeと明示的なz-indexを付けてテスト
小技:一時的に親要素から transform を外す、または isolation: isolate; を付けてSCを切るなどして挙動を確認するのも有効です。
それでもダメなときの最終兵器:<dialog> 要素やポータル
HTMLの <dialog> 要素(+ ::backdrop)を使えば、モーダル実装がシンプルになります。あるいは、フロントエンドフレームワークのポータル(Teleport)を使って、ルート直下に描画し、SC問題を根こそぎ回避するのも定番パターンです。
まとめ
- 「最前面に出したい」の鍵は
z-indexとスタッキングコンテキストの理解。 z-indexは 同じスタッキングコンテキスト内 でしか比較されない。transform,opacity < 1,filter,containなどが 新しいスタッキングコンテキストを作る。- 大規模案件では z-indexのスケール(設計指針)を決め、ポータルでDOMルート直下に出す のが安全。
- デバッグ時はDevToolsで
position/z-index/ 親要素のSCトリガーを丁寧に追う。
