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: isolate
will-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トリガーを丁寧に追う。