z-indexが効かないときの直し方:重なり順を決める5つの確認ポイント
z-index: 9999と書いても要素が前に出ないなら、数字の大きさよりもどの重なりのグループに属しているかを確認してください。
特に多い原因は、親要素が作った「スタッキングコンテキスト」です。子要素のz-indexをいくら大きくしても、親要素のグループごと背面にあれば、別グループの要素を追い越せません。
この記事では、ヘッダー、ドロップダウンメニュー、モーダル、ツールチップなどが隠れる問題を、最小コードで切り分けます。
先に確認するポイントは次の5つです。
positionとz-indexの組み合わせは適切か- 比較している要素は同じスタッキングコンテキストにいるか
- 親要素に
transformやopacityなどが付いていないか overflow: hiddenなどで切り取られていないか- モーダルなど、トップレイヤーを使うべきUIではないか
ここがポイント:
z-indexはページ全体で競う番号ではありません。同じスタッキングコンテキスト内の重なり順を決める番号です。
前提環境とz-indexの基本
通常の重なりなら、同じグループ内で大きいz-indexを指定した要素が手前に表示されます。
この記事は、2026年7月29日時点のHTMLとCSSを対象にしています。z-indexは主要ブラウザで広く利用できる基本機能ですが、FlexboxやGrid、position: sticky、<dialog>などでは初心者向けの古い説明だけでは判断できないケースがあります。
positionごとの違い
positionの初期値はstaticです。基本的な位置指定要素では、次のように考えると整理しやすくなります。
static: 通常の文書フローに配置される。一般的な要素ではtop、left、z-indexが効かないrelative: 元の位置を基準に移動できる。z-indexがauto以外ならスタッキングコンテキストを作るabsolute: 通常フローから外れ、直近の位置指定された祖先などを基準に配置される。z-indexがauto以外ならスタッキングコンテキストを作るfixed: 通常はビューポートを基準に固定され、常にスタッキングコンテキストを作るsticky: スクロール位置に応じて通常配置と固定配置のような動きをし、常にスタッキングコンテキストを作る
ただし、Flexboxの子要素とGridの子要素は例外です。これらはposition: staticのままでも、整数のz-indexを使って重なり順を変えられます。
最小の動作例
次の例では、2つのボックスを同じ親要素内で重ねています。
<div class="stage">
<div class="box box-a">A</div>
<div class="box box-b">B</div>
</div>
.stage {
position: relative;
height: 180px;
}
.box {
position: absolute;
width: 120px;
height: 120px;
padding: 16px;
color: #fff;
}
.box-a {
top: 20px;
left: 20px;
z-index: 2;
background: #2563eb;
}
.box-b {
top: 60px;
left: 60px;
z-index: 1;
background: #dc2626;
}
期待される表示は、青いAが赤いBより手前です。両方が同じスタッキングコンテキストにいて、2と1を直接比較できるためです。
z-indexを指定しない場合、重なり順には描画ルールとHTML上の順序が関係します。単純な位置指定要素どうしなら、後に書かれた要素が手前になるため、「CSSを変えていないのにDOMの並べ替えで表示が変わった」ということも起こります。
原因1:positionの指定だけを確認している
最初に見るべきなのは、対象要素がz-indexを適用できる条件にあるかどうかです。
よくあるNG例は、通常のブロック要素に数値だけを追加する書き方です。
.badge {
z-index: 10;
}
.badgeが通常の要素で、FlexboxやGridの子要素でもなければ、positionの初期値はstaticです。この場合、期待したようにz-indexで重なりを変更できません。
基本的な改善例は次のとおりです。
.badge {
position: relative;
z-index: 10;
}
position: relativeは、topやleftを指定しなければ見た目の位置を動かしません。そのため、既存のレイアウトを大きく変えずに重なり順を明示したい場合に使えます。
ただし、この修正で直らなければ、さらに大きい数字を追加する段階ではありません。次に親要素を調べます。
原因2:親のスタッキングコンテキストに閉じ込められている
z-indexが効かない問題の中心は、子要素ではなく親要素の重なり順にあることが少なくありません。
スタッキングコンテキストは、重なり順を計算する独立したグループです。グループ内の子要素を並べ終えると、ブラウザはそのグループ全体を一つのまとまりとして、親側の重なり順に配置します。
9999でも前に出ない例
<header class="header">
<button>メニュー</button>
<nav class="dropdown">メニュー内容</nav>
</header>
<main class="main-content">
メインコンテンツ
</main>
.header {
position: relative;
z-index: 1;
}
.dropdown {
position: absolute;
z-index: 9999;
}
.main-content {
position: relative;
z-index: 2;
}
.dropdownの9999は、.headerが作ったスタッキングコンテキストの内部でしか意味を持ちません。外側で比較されるのは、.headerの1と.main-contentの2です。
そのため、ドロップダウンは.headerと一緒に.main-contentの背面へ回ります。
修正は比較される親で行う
この構造を保つなら、親側の重なり順を変更します。
.header {
position: relative;
z-index: 3;
}
.main-content {
position: relative;
z-index: 1;
}
別の選択肢は、不要なスタッキングコンテキストを作っている指定を親から外すことです。また、ドロップダウンやツールチップを重なりの制約が少ない場所へ移す設計もあります。
判断するときは、次の順で比較してください。
- 隠れている要素と、覆っている要素を特定する
- それぞれからDOMツリーを親方向へたどる
- 最初に見つかるスタッキングコンテキストを確認する
- 子の数値ではなく、そのコンテキストを作る要素どうしを比較する
親が背面にいる限り、子のz-index: 10を999999へ変えても結果は同じです。
原因3:transformやopacityが別グループを作っている
position以外のCSSプロパティもスタッキングコンテキストを作るため、意図しない親要素が重なりの境界になることがあります。
代表例は次のとおりです。
opacityが1未満transform、scale、rotate、translateがnone以外filterやbackdrop-filterがnone以外perspectiveがnone以外mix-blend-modeがnormal以外isolation: isolatecontain: layoutまたはcontain: paintを含む指定will-changeで、スタッキングコンテキストを作るプロパティを予告しているcontainer-type: sizeまたはinline-sizez-indexがauto以外のFlexbox/Gridの子要素
一覧を暗記するより、アニメーション、半透明、フィルター、コンテナクエリ、描画最適化を親要素へ追加した後に表示が変わったら疑う、と覚えるほうが実務的です。
見落としやすいtransformの例
<div class="card">
<button class="help-button">?</button>
<div class="tooltip">補足説明</div>
</div>
<div class="sidebar">サイドバー</div>
.card {
transform: translateZ(0);
}
.tooltip {
position: absolute;
z-index: 1000;
}
.sidebar {
position: relative;
z-index: 1;
}
.cardのtransformがスタッキングコンテキストを作ります。ツールチップはその内部から抜け出せず、.card全体の重なり位置によっては.sidebarの背面に表示されます。
translateZ(0)は、以前から描画の調整目的で使われることがある指定です。しかし、見た目の移動がなくても重なりの境界は生まれます。目的がないなら削除し、必要なら.card側の重なり順またはツールチップの配置先を設計し直します。
.card {
position: relative;
z-index: 2;
/* 不要なら transform: translateZ(0); を削除 */
}
will-change: transformも同様に注意が必要です。常時付ける最適化スイッチではないため、実際に変化する要素へ必要な期間だけ適用します。
原因4:z-indexではなくoverflowで切れている
要素が途中で消える場合は、重なり順ではなく親要素の表示領域からはみ出した部分が切り取られている可能性があります。
たとえば、カード内のメニューを外側へ広げる実装を考えます。
.card {
position: relative;
overflow: hidden;
}
.menu {
position: absolute;
top: 100%;
right: 0;
z-index: 100;
}
.menuの重なり順が高くても、.cardの領域外へ出た部分はoverflow: hiddenで切られます。z-indexはクリッピングを解除するプロパティではありません。
切り分け方
開発者ツールで、該当する祖先のoverflowを一時的に無効化します。それでメニュー全体が見えれば、原因は重なり順ではなく切り取りです。
修正方法は目的によって変わります。
- 切り取りが不要なら
overflow: visibleにする - 角丸画像だけを切り取りたいなら、画像用の内側要素に
overflow: hiddenを移す - メニューをカード外へ出すなら、切り取り対象の外側へ配置する
- JavaScript UIなら、オーバーレイを
body直下などへ描画するポータル方式を検討する
なお、overflowの変更はスクロール領域やレイアウトにも影響します。単純にvisibleへ変更する前に、元の指定が画像の切り抜き、横スクロール防止、スクロールコンテナのどれを目的としているか確認してください。
原因5:モーダルがブラウザのトップレイヤーより下にいる
モーダルやポップオーバーでは、巨大なz-indexで競争するより、HTMLが備えるトップレイヤーを使うほうが堅牢です。
トップレイヤーは、通常の文書内のスタッキングコンテキストより上にブラウザが用意する層です。代表的な対象には、次の要素があります。
showModal()で開いた<dialog>showPopover()で表示したPopover要素- フルスクリーン表示中の要素
通常の要素にz-index: 2147483647を指定しても、トップレイヤーに表示されたモーダルを追い越すための解決策にはなりません。
dialogを使った最小例
<button id="open-dialog">確認画面を開く</button>
<dialog id="confirm-dialog">
<p>この内容で続けますか?</p>
<button id="close-dialog">閉じる</button>
</dialog>
#confirm-dialog {
max-width: 32rem;
border: 0;
border-radius: 12px;
padding: 24px;
}
#confirm-dialog::backdrop {
background: rgb(0 0 0 / 55%);
}
const dialog = document.querySelector('#confirm-dialog');
document.querySelector('#open-dialog').addEventListener('click', () => {
dialog.showModal();
});
document.querySelector('#close-dialog').addEventListener('click', () => {
dialog.close();
});
dialog.show()では通常の非モーダルダイアログとして表示されます。トップレイヤーへ載せるモーダル表示にはshowModal()を使う点が重要です。
ただし、トップレイヤーを使えばUI設計がすべて完成するわけではありません。閉じる操作、初期フォーカス、処理後のフォーカス復帰、Escキーで閉じた場合の扱い、背景操作を止める必要性まで確認してください。
開発者ツールで原因を順番に探す
最短のデバッグ方法は、数値を増やすことではなく、覆っている要素とスタッキングコンテキストの境界を特定することです。
ブラウザによって表示名や機能は異なりますが、ElementsやInspectorに相当する画面でDOMと適用済みCSSを確認できます。
1. 本当に重なっているか確認する
隠れている要素を選択し、次を見ます。
- 要素自体がDOMに存在するか
display: noneやvisibility: hiddenになっていないかopacity: 0になっていないか- 幅や高さが0になっていないか
- 想定した位置に配置されているか
要素が別の場所に配置されているなら、z-indexではなくtop、left、inset、基準となる祖先などの問題です。
2. 覆っている要素を特定する
一時的に背景色や輪郭を付けると、領域を確認しやすくなります。
.debug-target {
outline: 3px solid magenta;
background-color: rgb(255 0 255 / 15%);
}
検証用の指定は公開コードに残さず、問題の要素がどこまで広がっているかを見るために使います。
3. 両方の親を上へたどる
隠れている要素と覆っている要素について、祖先の次の指定を確認します。
positionとz-indextransformopacityfilterisolationcontaincontainer-typewill-changeoverflow
最初に見つかった境界が異なるなら、子要素のz-indexを直接比較できない可能性があります。
4. 疑わしい宣言を一つずつ無効化する
複数のCSSをまとめて削除すると、どれが原因だったか分からなくなります。開発者ツール上で一つずつ切り替え、表示が変わる宣言を探してください。
確認後は、単にその宣言を削除するだけでなく、次のどれが適切かを判断します。
- 親側の
z-indexを調整する - 不要なスタッキングコンテキストをなくす
- オーバーレイのDOM上の配置先を変える
- クリッピング用の要素を分ける
<dialog>やPopover APIへ置き換える
実務ではz-indexを役割ごとに管理する
場当たり的な巨大数を避け、UIの役割ごとに少数のレイヤーを決めると、後から重なり順を追いやすくなります。
たとえば、サイト全体で次のような段階を用意します。
:root {
--z-base: 0;
--z-header: 100;
--z-dropdown: 200;
--z-overlay: 300;
--z-modal: 400;
--z-toast: 500;
}
.site-header {
position: sticky;
top: 0;
z-index: var(--z-header);
}
.dropdown {
position: absolute;
z-index: var(--z-dropdown);
}
この方法には、「9999なら常に最前面」という保証はありません。スタッキングコンテキストが異なれば、CSS変数で管理していても子要素どうしの数値は直接比較されないためです。
それでも、役割を名前にする価値はあります。
- 数字を見ただけでは分からないUI上の目的が残る
- 同じ階層のコンポーネント間で値をそろえやすい
- 新しいUIをどの層へ置くか判断しやすい
100000、999999と数値だけが膨らむ状態を防げる
コンポーネント内部の重なり順は、小さい値で完結させるのも有効です。
.card-image {
position: relative;
z-index: 0;
}
.card-badge {
position: absolute;
z-index: 1;
}
サイト全体のレイヤーと、カード内部のレイヤーを同じ数値体系で管理する必要はありません。どのスタッキングコンテキスト内で比較するかを先に決めることが重要です。
よくある修正と使い分け
正しい修正は、隠れた原因が「適用条件」「親の重なり」「切り取り」「UIの配置先」のどれかで変わります。
position: relativeを追加する
対象要素が通常の要素で、同じ階層にある要素との重なりだけを変えたいときに使います。
.target {
position: relative;
z-index: 2;
}
親のスタッキングコンテキストが背面にある問題や、overflowによる切り取りは解消できません。
親のz-indexを変更する
子要素が別のスタッキングコンテキストに閉じ込められている場合に有効です。ただし、ヘッダー全体を前へ出すと、ヘッダー内の背景やボタンもまとめて前面になります。
影響する範囲を画面全体で確認してください。
DOM上の配置先を変える
ツールチップ、ドロップダウン、日付選択パネルなどが複数の親による切り取りや重なりの制約を受ける場合に適しています。
一方で、基準要素への追従、スクロール時の位置更新、イベント処理、アクセシビリティ上の関連付けが必要になります。配置先を変えるだけで終わらせず、操作まで確認します。
dialogやPopoverを使う
モーダルや一時的な前面UIに向いています。通常のz-index競争から切り離せることが利点です。
ただし、すべての装飾要素をトップレイヤーへ置く必要はありません。カード内のバッジや画像上のラベルなど、コンポーネント内で完結する重なりには通常のz-indexを使います。
z-indexが効かないときの確認チェックリスト
修正前に次の順で調べれば、巨大な数値を試し続ける作業を避けられます。
- [ ] 隠れている要素はDOMに存在し、表示状態になっている
- [ ] 対象要素の位置とサイズは想定どおりである
- [ ] 通常の要素なら
positionがstatic以外になっている - [ ] Flexbox/Gridの子要素に該当するか確認した
- [ ] 覆っている要素を特定した
- [ ] 両方の要素から親方向へスタッキングコンテキストを調べた
- [ ] 親の
transform、opacity、filterなどを確認した - [ ] 祖先の
overflowで切り取られていない - [ ]
<dialog>やPopoverなどトップレイヤーとの競合ではない - [ ] 修正後にヘッダー、メニュー、モーダル、通知など他の前面UIも確認した
最初に直すべき場所は、z-index: 9999が書かれた子要素とは限りません。比較されている親要素を見つけ、同じ階層の値を直すことが、再発しにくい解決への近道です。
