Woman checking her email in a meeting
「モーダルが背景の下に潜ってしまう」「ドロップダウンがヘッダーより後ろに隠れる」──フロントエンド実装で頻出する“最前面に出したいのに出ない”問題の多くは、z-index と “スタッキングコンテキスト(stacking context)” を正しく理解していないことが原因です。この記事では、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番目の「スタッキングコンテキスト」が、混乱の元になります。
スタッキングコンテキスト(以下、SC)は「重なり順の判定グループ」のようなものです。異なるSC同士では、いくら z-index を大きくしても越えられないケースがあります。
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に属する要素の前面には出られません。
<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なので、勝てないことがある */
}
解決策
transform)を外す。body直下に配置し、最上位レイヤーで扱う(ポータル/teleport戦略)。親に transform が付いていると、その子はSCに閉じ込められます。ドロップダウンをポータルでDOM上位に描画するか、親の transform を回避する(transform を使わずにGPUアクセラレーションを切るなど)必要があります。
よく使われる例:
: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);
}
transform: translateZ(0) や opacity: 0.999 といった“なんとなく最適化”でSCを増やすと、可視化の混乱が起きます。必要な箇所だけに絞る運用を。
React/ Vue などでは、ポータル(createPortal, Teleport)を使い、<body> 直下にモーダルやドロップダウンを出すのが定番。これによりSCのネスト問題を避けられます。
position: static:z-index 無効(指定しても効かない)。position: relative / absolute / fixed / sticky:z-index 有効。position: fixed:ビューポート基準になるため、他要素と被りやすく、しばしば最前面として機能する。ただし、親のSCに縛られるケースもある点に注意。CSS Cascade Layers(@layer)は「カスケード優先順位(どのスタイルが勝つか)」を制御する仕組みで、描画の重なり順(z軸)とは無関係です。@layer で宣言順を管理しても、要素の最前面/背面は変えられない点を混同しないようにしましょう。
z-index を確認z-index が auto になっていないか、position が static ではないかを確認transform や opacity < 1 などSCを作っていないかチェックposition: relativeと明示的なz-indexを付けてテスト小技:一時的に親要素から transform を外す、または isolation: isolate; を付けてSCを切るなどして挙動を確認するのも有効です。
HTMLの <dialog> 要素(+ ::backdrop)を使えば、モーダル実装がシンプルになります。あるいは、フロントエンドフレームワークのポータル(Teleport)を使って、ルート直下に描画し、SC問題を根こそぎ回避するのも定番パターンです。
z-index とスタッキングコンテキストの理解。z-index は 同じスタッキングコンテキスト内 でしか比較されない。transform, opacity < 1, filter, contain などが 新しいスタッキングコンテキストを作る。position / z-index / 親要素のSCトリガーを丁寧に追う。