「後ろに隠れてしまう要素を前面に出したくてz-index: 9999まで上げたのに、それでも隠れたまま」という相談は珍しくありません。数値を大きくすればするほど勝てるはず、という感覚は自然ですが、CSSのz-indexはそう単純には動きません。

z-indexは「同じグループの中」でしか比較されない

z-indexの値は、ページ全体でランキングされているわけではありません。比較されるのは「同じスタッキングコンテキスト(重なりの文脈)」に属する要素同士だけです。

スタッキングコンテキストは、以下のようなCSSプロパティを持つ要素に生まれます。

  • position: relative / absolutez-index(autoでない値)
  • position: fixed / sticky(z-indexを指定していなくても、これだけで発生する)
  • opacity が1未満
  • transform がnone以外
  • filter がnone以外

これらのどれか1つでも親要素に付いていると、その親は新しいスタッキングコンテキストの「箱」になります。箱の中の子要素がどれだけz-indexを盛っても、比較対象は箱の中だけです。箱そのものの重なり順は、箱の外側にある要素と比べたときの箱自身のz-index(または出現順)で決まります。

具体例で見る

次のようなHTML/CSSがあるとします。

<header class="site-header">サイトヘッダー</header>
<main>
  <div class="card">
    <span class="badge">NEW</span>
  </div>
</main>
.site-header {
  position: sticky;
  top: 0;
  z-index: 1; /* ヘッダーを常に手前に固定表示させたい */
}
.card {
  position: relative;
  opacity: 0.99; /* フェードイン演出用にうっすら透過をかけていた */
}
.badge {
  position: absolute;
  z-index: 9999;
}

.badgeのz-indexは9999、ヘッダーはたったの1です。数字だけ見れば.badgeが圧勝しそうですが、スクロールして.cardがヘッダーの下を通り過ぎると、.badgeはヘッダー(z-index 1)の下に隠れてしまいます。.badgeのz-indexをいくら大きくしても、この重なりは変わりません。

理由は、.cardopacity: 0.99が付いているせいで.card自身が独立したスタッキングコンテキストになっているからです。.badgeのz-index 9999は、あくまで.cardという箱の中でのランキングでしかありません。箱の外から見ると、.card自身はz-indexを明示していない(=auto扱い)ため、ページ全体の重なり順を決める場では「z-indexを明示していない要素」の扱いになります。一方の.site-headerz-index: 1という正の値を明示しているため、ページ全体の重なり順では.site-headerの方が優先され、中に9999を持つ要素がいようと.cardの箱ごと下に沈みます。

直し方の選択肢

  1. 不要なスタッキングコンテキストを作らない: opacity: 0.99のように「見た目にはほぼ影響しない値」が、意図せず.cardをスタッキングコンテキストにしてしまっている。本当に必要な演出か確認し、不要なら外す(外すだけで.badgeのz-index 9999が.site-headerと直接比較されるようになり、正しく前面に出ます)
  2. 箱ごと持ち上げる: .badgeではなく、箱になっている.card自身に.site-headerより大きいz-indexを指定して、箱同士の重なり順で解決する
  3. DOM構造を変える: モーダルやトースト通知のように「常に一番手前に出したい」要素は、そもそも別のスタッキングコンテキストに閉じ込められないよう、document.body直下など浅い階層に配置し直す(ReactであればcreatePortalを使う手もあります)

とくに3つ目は、モーダルを実装したときに「z-indexを最大値にしても他の要素の後ろに回り込む」という詰まりで見かけることがあります。原因が数値の大小ではなく「箱の中に閉じ込められているかどうか」にあると分かると、z-indexをどこまで上げても直らない理由に納得がいくはずです。

まとめ

  • z-indexは同じスタッキングコンテキストの中でしか比較されない
  • position+z-index以外にも、opacitytransformfilterなど「見た目の演出のため」に付けたプロパティが意図せずスタッキングコンテキストを作ることがある
  • 直らないときは「数値をさらに上げる」前に、そもそも比較対象になっているかを疑う

面談なしで、今日から始められます

この講座は 未経験から Next.js + Supabase + Claude Code で Webアプリを公開するまで を全24セッションで体系化した教材付きプランです。無料相談を挟まず、申し込んだその日から教材で学習を始められます。AIが学習パートナーになって何度でも質問でき、つまずいた所だけチャットで直接サポートします。

  • 今日から始める(教材完全版+月5,500円・チャット質問し放題・いつでも解約OK)→ https://menta.work/plan/20251?ref=menta-knowledge
  • いきなりは不安な方へ:無料の教材体験版もあります(最初の数セッション分)→ プラン詳細をご覧ください