「z-index: 9999; を指定したのに、その要素が前面に出てこない」——これ、私もWeb制作を始めてから何度も食らってきた、CSSのつまずきポイントです。
z-indexは要素の重なり順を決めるプロパティなので、数字を大きくすれば一番手前に来るはず、と考えるのが自然です。ところが現場では、こんなことが平気で起こります。
- z-index: 9999; にしているのに前面に出ない
- ヘッダーよりモーダルが後ろに隠れてしまう
- ドロップダウンメニューが画像やスライダーの下に潜り込む
- position: fixed; を付けたのに思った位置で重ならない
こうなると、つい z-index の数字をさらに盛って 999999 にしたくなります。でも、それで直ることはほとんどありません。z-indexは単純な「数字の大きさ比べ」ではないからです。本当に効いてくるのは、その要素がどの重なり順グループ(重ね合わせコンテキスト)に属しているか、そして position がきちんと指定されているかという2点です。
この記事では、z-indexが効かなくなる主な原因を仕組みから順に切り分け、私が実際に使っている確認手順と修正パターンまでまとめて解説します。原因さえ見えれば、巨大な数字に頼らなくても重なり順はコントロールできます。
z-indexとは何か
z-indexは、重なり合った要素のどちらを手前に表示するかを決めるCSSプロパティです。
横方向をX軸、縦方向をY軸とすると、画面の手前と奥に向かう方向がZ軸です。z-indexはこのZ軸の並び順を指定します。基本ルールはシンプルで、同じグループの中なら値が大きい要素ほど手前に来ます。
.box-a {
position: relative;
z-index: 1;
}
.box-b {
position: relative;
z-index: 10;
}
このコードなら、条件が同じである限り .box-b のほうが前面に表示されます。
ここで効いてくるのが「条件が同じである限り」という前提です。実際のページでは、親要素や position、transform、opacity といった他のCSSが絡んで、数字を大きくしただけでは前に出てこないケースが頻発します。原因はだいたい決まっています。
z-indexが効かない主な原因
z-indexが意図通りに動かないとき、犯人になりやすいのは次のあたりです。
- position が指定されていない(static のまま)
- 親要素の重なり順グループに閉じ込められている
- 別の重ね合わせコンテキスト(stacking context)が作られている
- 親要素の transform や opacity、filter が影響している
- HTMLの配置順・親子関係が想定とずれている
原因1:positionが指定されていない
まず大前提として、z-indexはすべての要素に常に効くわけではありません。効くのは、おおまかに言うと次の2タイプです。
- position が static 以外(relative / absolute / fixed / sticky)の要素
- フレックスアイテム・グリッドアイテム(親が display: flex または display: grid の子要素)
つまり、position を付けず static のままの要素に z-index だけ書いても、基本的には無視されます。次のコードがその典型です。
.menu {
z-index: 100; /* これだけでは効かないことが多い */
}
この場合は position を一緒に指定します。
.menu {
position: relative;
z-index: 100;
}
ヘッダー、ドロップダウン、ボタンなどで z-index が効かないときは、まず position が指定されているかを真っ先に確認してください。なお、フレックスやグリッドのアイテムは例外で、position が static のままでも z-index が効きます。レイアウトに Flexbox や Grid を使っている要素なら、ここでつまずくことは少ないはずです。
position そのものの挙動が怪しいときは、基礎をおさらいしておくと切り分けが速くなります。CSSでposition:relativeを使った右寄せの方法と応用もあわせてどうぞ。
参考:MDN:z-index
原因2:親要素の重なり順に閉じ込められている
私が一番ハマってきたのがこれです。子要素にどれだけ大きな z-index を指定しても、親要素が低い重なり順のグループに入っていると、子はその親の枠を飛び越えて別の親の上に出ることはできません。
次のような構造を考えてみます。
<div class="area-a">
<div class="modal">モーダル</div>
</div>
<div class="area-b">
<div class="header">ヘッダー</div>
</div>
CSSはこうなっているとします。
.area-a {
position: relative;
z-index: 1;
}
.area-b {
position: relative;
z-index: 10;
}
.modal {
position: absolute;
z-index: 9999;
}
直感的には z-index: 9999; のモーダルが最強に見えます。ところが、モーダルの重なり順はまず親 .area-a(z-index: 1)の中で確定し、その .area-a 自体が .area-b(z-index: 10)より後ろにいるため、結果としてモーダルは .area-b の中のヘッダーより後ろに沈みます。9999という数字は、あくまで .area-a の内部での順位でしかないわけです。
親同士の順番で勝負が決まっている以上、子の数字をいくら盛っても逆転しません。直すなら、モーダルを親の入れ子から出して body 直下に近い位置へ動かすか、親側の z-index 設計を見直すのが筋です。
原因3:知らないうちにstacking context(重ね合わせコンテキスト)が作られている
原因2をきちんと理解する鍵が、この「stacking context(重ね合わせコンテキスト)」です。
stacking contextは、ざっくり言えば「重なり順を独立して管理するグループ」のことです。同じグループの中なら z-index で順位を比べられますが、別々のグループに入った要素同士は、いくら片方の数字が大きくても直接は比較されません。グループの外枠(=親が属するグループの順位)が先に効いてしまうのです。原因2で 9999 が負けたのは、まさにこの仕組みのせいでした。
やっかいなのは、stacking context が z-index だけで作られるわけではない点です。MDNによれば、主に次のような場合に新しいグループが生まれます。
- position: absolute / relative で、かつ z-index が auto 以外
- position: fixed または sticky(z-index の指定がなくても作られる)
- opacity が 1 未満
- transform / scale / rotate / translate が none 以外
- filter / backdrop-filter / perspective / clip-path / mask が指定されている
- isolation: isolate; が指定されている
- mix-blend-mode が normal 以外
- will-change に上記を生むプロパティが指定されている
- container-type が size または inline-size
つまり、z-index を一切書いていない親要素でも、transform や opacity がきっかけで新しいグループになり、その中に子が閉じ込められることがあります。アニメーションやスライダー、画像エフェクトが当たり前になった今のサイトでは、これが本当に多い。「z-index は正しく書いたのに効かない」と感じたら、まず親をたどって、stacking context を作っていそうなプロパティが付いていないか探すのが近道です。
原因4:親要素のtransformに足をすくわれる
原因3の中でも特に見落としやすいのが、親要素に transform が当たっているケースです。
.wrapper {
transform: translateZ(0); /* GPU描画のおまじない等でよく付ける */
}
.fixed-menu {
position: fixed;
z-index: 9999;
}
.wrapper に transform が付くと、その中の position: fixed; はビューポート(画面全体)ではなく .wrapper を基準に配置されるようになります。これは仕様で決まった挙動で、「固定したはずのメニューが画面に張り付かない」「重なり順がおかしい」の原因になりがちです。スクロール追従やパフォーマンス対策で translateZ(0) や will-change を入れた覚えがあるなら、まずそこを疑ってください。
fixed まわりの細かい挙動は、【CSS】position:fixedで固定している要素を横スクロールさせる方法でも触れています。
原因5:opacityやfilterが影響している
opacity を 1 未満にしているだけでも stacking context は作られます。
.card {
opacity: 0.99; /* 見た目はほぼ変わらないが重なり順には効く */
}
opacity: 0.99; なんて見た目にはまず気づきません。でもCSSの内部ではこの要素が独立したグループになり、中の子要素の z-index は外と比較されなくなります。filter(ぼかしや色調整)も同じです。画像のフェード演出、ホバーエフェクト、ぼかし処理を入れている要素は、opacity と filter の指定を確認してみてください。
よくある修正例
ヘッダーを常に前面に出したい
追従する固定ヘッダーを前面に保つなら、次のように書きます。
.site-header {
position: sticky;
top: 0;
z-index: 1000;
background: #fff;
}
position: sticky; を使えば、JavaScriptなしでスクロール追従ヘッダーが作れます。ただし、祖先要素に overflow: hidden; や overflow: auto; があると sticky が効かなくなるので、効かないときは親の overflow を確認してください。
モーダルを最前面に出したい
モーダルは、できるだけ body 直下に近い場所へ置くのが鉄則です。深い入れ子に入れるほど、原因2・3の親グループに足を引っ張られます。
.modal-overlay {
position: fixed;
inset: 0;
z-index: 9999;
background: rgba(0, 0, 0, 0.6);
}
.modal-content {
position: relative;
z-index: 10000;
}
どうしても深い位置にしか置けないときは、後述する isolation: isolate; を使ってグループ設計を整理する手もあります。
ドロップダウンメニューが画像やスライダーに隠れる
ドロップダウンが下に潜るときは、メニュー側だけでなく、隠している相手(メインビジュアルやスライダー)の transform / z-index も両方確認します。
.global-nav {
position: relative;
z-index: 1000;
}
.dropdown {
position: absolute;
z-index: 1001;
}
それでも潜るなら、相手のスライダーに transform が付いて別グループになっている可能性が高いです。原因4・5を思い出してください。
isolation: isolate; でグループを明示的に作る
現代的な解決として覚えておきたいのが isolation: isolate; です。これは「ここから下を独立した重ね合わせコンテキストにする」と明示的に宣言するプロパティで、position も z-index も使わずに stacking context を作れます。
.widget {
isolation: isolate;
}
たとえばコンポーネントを部品として配り回すとき、その内部の z-index がページ全体の巨大な数字と干渉しないよう、ルート要素に isolation: isolate; を付けておく。こうすれば内部の重なり順は外と切り離され、「相手のサイトの z-index: 999999 に負ける」といった事故を防げます。z-index のインフレ合戦から抜け出すための、地味だけれど効く一手です。
z-indexが効かない時の確認手順
原因が見えないときは、次の順で切り分けると早いです。私もこの順番でたどっています。
- 対象要素に position が指定されているか(static のままでないか)を確認する
- 対象要素の z-index が実際に適用されているか、開発者ツールで確認する
- 親・祖先要素に z-index が指定されていないか確認する
- 親・祖先要素に transform / opacity / filter / will-change がないか確認する
- isolation や mix-blend-mode、container-type の指定も確認する
- 原因が親側にあるなら、HTML構造を見直して要素を浅い位置へ移す
ブラウザの開発者ツールには、要素を選ぶとどの stacking context に属しているかを示す機能もあります。z-index: 999999; のような極端な数字を入れる前に、まず構造を見たほうが圧倒的に早く解決します。
まとめ
z-indexが効かない原因は、数値が小さいからとは限りません。今日の要点を絞ると次の通りです。
- z-index は position(static以外)とセットで考える。例外はフレックス・グリッドアイテム
- 子の数字は親の重なり順グループの中での順位でしかない
- transform・opacity・filter などが、知らないうちに stacking context を作る
- 親要素の transform は position: fixed; の基準まで変えてしまう
- 巨大な数字に頼らず、isolation: isolate; でグループを設計する手がある
もし一つだけ持ち帰るなら、「効かないと思ったら、まず自分ではなく親要素を疑う」です。前面に出したい要素そのものより、その親や周辺要素のCSSにこそ答えが隠れています。数字を盛る前に親をたどる——この癖がつくと、z-indexのトラブルはぐっと減ります。