結論

z-index が効かないとき、プロは数字を見ない——「この要素はどのスタッキングコンテキストに閉じ込められているか」を疑う。z-index は同じコンテキスト内の兄弟としか比較されないローカルな値なので、親の opacity:0.99transform が作ったコンテキストの中では z-index:999999 でも外には出られない。解は2つ——isolation:isolate で副作用ゼロのコンテキストを意図的に作り、すべての z-index は CSS変数の命名トークン階段から引く。そして「elevation(影=奥行きの見た目)」と「z-index(重なり順)」を別物として設計する。

01 — まず「どのフォルダに閉じ込められたか」を疑う

ブラウザはまず親コンテキスト(フォルダ)を並べ、その中で子(紙)を並べる。だから子は別フォルダの紙の間に割り込めない。tooltip:999999header:2 の下に潜るのは、親 mainposition:relative; z-index:1 が付いてツールチップがローカルコンテキストに封印されているから。数字を上げる前に、効かない要素の祖先を DevTools で遡って opacity<1 / transform / position:fixed を探すのが第一手。

header z:2
main · z:1
(opacity:.99)
tip z:999999
✗ 親mainが opacity:.99 でコンテキスト化 → tipは999999でもmain内に封印され、headerの下
header z:2
main · z:1
(opacity:1)
tip z:5
✓ 余計なコンテキストを外す → tipはたった z:5 でheaderの上に出る

blogWhat The Heck, z-index?? — Josh W. Comeau

primaryStacking context — MDN Web Docs

02 — コンテキストは isolation:isolate で意図的に作る

opacity:0.99transform:translateX(0)(実質no-op)でうっかりコンテキストを作るのをやめ、isolation:isolate を使う。これは「新しいスタッキングコンテキストを作る」だけの仕事をし、視覚的副作用ゼロ・position 不要・z-index値の指定も不要。コンポーネント内部の重なりをローカルに閉じ込められるので、外側の z-index 戦争に巻き込まれない再利用可能な部品になる。z-index 自体は !important と同じ「最後の手段」と捉える。

隣のUI
z:40
NEW
✗ isolationなし → カード内のbadge(z:50)が外の兄弟(z:40)とグローバルに殴り合う
隣のUI
z:1
NEW
✓ isolation:isolate → badgeの重なりはカード内で完結、外へ漏れない(値は指定不要)

blogWhat The Heck, z-index?? — Josh W. Comeau

secondaryThe Value of z-index | CSS-Tricks

03 — 魔法の数字をやめ、命名トークンの階段から引く

99910001 を直書きすると、後続が読めなくなり「z-index 軍拡競争」が始まる。少数・広間隔の命名トークンを :root に定義し、すべての z-index はそこを参照する。Atlassian の実ラダー(nav 200 → dropdown 300 → modal 510 → tooltip 800)は100刻みで、後から層を差し込める。値の大小は本質ではなく「単一の命名トークン源から引く」ことが肝。

nav: 100modal: 9999toast: 99999tooltip: 2147483647
✗ 各所で盛った任意値 → 序列が一望できず、新しい層を入れる隙間が読めない
nav 200
dropdown 300
modal 510
tooltip 800
✓ 100刻みの命名階段 → 序列が一望でき、間に層を挿せる(Atlassian実値)

secondaryThe Value of z-index | CSS-Tricks

primaryOverview - Elevation - Atlassian Design

04 — elevation(影)と z-index(重なり順)を分離する

「浮いて見える(elevation=影スタイル)」と「DOM上で前にある(z-index=重なり順)」は別概念。同じ elevation を共有する2要素でも z-index は別々に振る必要がある。混同すると「影は浮いているのに重なり順が逆」という安っぽい破綻が出る。Atlassian は surface トークンと shadow トークンのペア必須を明文化、Material は影を奥行きの見た目として z-index と独立管理する。

強い影
(浮いてる風)
z:2 で
手前
✗ 影は強い=手前に見えるのに z-index は下 → 影と重なり順が矛盾し破綻
card
shadow-1 / z-base
popover
shadow-3 / z-popup
✓ 影トークンと z-index トークンをペアで設計 → 浮きと順序が一致

primaryElevation — Material Design (M2)

primaryOverview - Elevation - Atlassian Design

05 — モーダルは top layer に逃がして z-index 戦争から降りる

position:fixed/sticky なヘッダーは z-index 不要で常にコンテキストを作るので、固定ヘッダー自身が「フォルダ」になり外側のモーダルとの上下が数字どおりにならない。open 状態の <dialog> や Popover API は、ブラウザの top layer に昇格してページの z-index 序列を完全に飛び越え最前面に出る。モーダル/トーストでラダーを管理する手間が減る方向。ただしレガシー対応やフォーカストラップは別途必要。

sticky header
modal z:1
(headerの下)
✗ stickyヘッダーがコンテキスト化 → 普通のmodalは数字を盛ってもヘッダーに勝てない
sticky header
<dialog>
top layer ↑
✓ <dialog>/Popover APIはtop layerへ昇格 → z-index不要でヘッダーの上に出る

primaryStacking context — MDN Web Docs

secondaryUnstacking CSS Stacking Contexts — Smashing Magazine

実装スニペット

z-index トークン階段(CSS変数・100刻み、将来挿入の余地あり)。Atlassian の実ラダー値を採用し、直書きの 999/10001 を全廃する。

:root {
  /* グローバルz-indexラダー:全z-indexはここから引く */
  --z-base:        0;
  --z-nav:         200;
  --z-dropdown:    300;
  --z-popup:       400;
  --z-blanket:     500;  /* モーダル背景の幕 */
  --z-modal:       510;
  --z-flag:        600;  /* トースト/通知 */
  --z-spotlight:   700;
  --z-tooltip:     800;
}

.site-header    { z-index: var(--z-nav); }
.modal          { z-index: var(--z-modal); }
/* 一緒に動く要素はcalc()で相対に縛る:間に割り込ませない */
.modal__blanket { z-index: calc(var(--z-modal) - 1); }
.tooltip        { z-index: var(--z-tooltip); }

isolation:isolate でコンポーネントの z-index をローカルに閉じ込める。opacity:0.99transform の代わりにこれを使い、無意見で再利用可能な部品にする。

/* このカードは『フォルダ』になる:中のz-indexは外へ漏れず、
   外のz-index戦争にも巻き込まれない。視覚的副作用ゼロ・position不要 */
.card {
  isolation: isolate;
}
.card__badge   { position: absolute; z-index: 2; } /* カード内でだけ効く */
.card__overlay { position: absolute; z-index: 1; }
/* z-index値を一切指定せずコンテキストを作れるのが利点。IE以外の全ブラウザで動作 */

スタッキングコンテキストを静かに作る要注意プロパティ(MDN準拠・バグ調査チェックリスト)。「z-index が効かない」時は DevTools で効かない要素の祖先を遡り、これらを探す。

/* これらが祖先にあると、子のz-indexはその中に封印される=バグ源 */
.x { opacity: 0.99; }                 /* opacity < 1 で発生(1なら発生しない) */
.x { transform: translateX(0); }      /* no-opでも発生。scale/rotate/translateも */
.x { filter: blur(0); }               /* filter/backdrop-filter != none */
.x { mix-blend-mode: multiply; }      /* normal以外で発生 */
.x { position: fixed; }               /* fixed/sticky はz-index不要で常に発生 */
.x { will-change: transform; }        /* opacity/transform指定で発生 */
.x { contain: paint; }                /* layout/paint/strict/content */
.x { container-type: inline-size; }   /* コンテナクエリ導入の隠れトラップ */
/* flex/grid の子は position 無しでも z-index:0 でコンテキスト生成 */

elevation(影)と z-index(重なり順)を分離して持つ。影=奥行きの「見た目」、z-index=重なりの「順序」を独立管理する。

:root {
  /* elevation = 影スタイル(Material M3: 0/1/3/6/8/12dp 相当) */
  --shadow-1: 0 1px 2px rgba(0,0,0,.10);   /* card */
  --shadow-3: 0 6px 12px rgba(0,0,0,.14);  /* menu/popover */
  --shadow-5: 0 12px 24px rgba(0,0,0,.18); /* dialog */
}
/* 同じ影でも重なり順は別トークンで振る */
.card    { box-shadow: var(--shadow-1); z-index: var(--z-base); }
.popover { box-shadow: var(--shadow-3); z-index: var(--z-popup); }
.dialog  { box-shadow: var(--shadow-5); z-index: var(--z-modal); }

チェックリスト

  • z-index が効かないとき、数字を上げる前に DevTools で祖先を遡り、コンテキスト生成プロパティ(opacity<1 / transform / filter / position:fixed/sticky / will-change / contain / container-type)を探したか
  • コンポーネント内部の重なりは isolation:isolate でローカル化したか(opacity:0.99 や transform で偶発的に作っていないか)
  • すべての z-index を :root の命名トークン(var(--z-*))から引いているか。999/10001 の直書きはゼロか
  • レイヤー序列(ラダー)を1か所に明文化し、プロジェクトで100刻み派か5刻み低値派のどちらか一方に統一したか
  • 一緒に動くペア(モーダル+背景幕など)は calc(var(--z-modal) - 1) で相対に縛り、間に割り込めないようにしたか
  • elevation(影トークン)と z-index(重なり順トークン)を別物として振り、「影は浮くのに順序が逆」になっていないか
  • 最前面に出すべきモーダル/トーストは <dialog>/Popover API の top layer を検討したか(フォーカストラップ等 a11y は別途)

限界 / 出典

注意:原典は主に英語圏のデザインシステム/解説で、日本語LP・バナー特化の検証ではない(ただし z-index/スタッキングコンテキストの挙動はブラウザ仕様なので言語非依存で適用可)。2つのトークン戦略(100刻みの CSS-Tricks/Atlassian 派 vs 5刻み・低値の OutSystems 派)は思想が相反するため、どちらか一方をプロジェクトで統一する必要があり「両方とも正解」ではない。Atlassian のラダー実値(nav200〜tooltip800)や USWDS の top:99999 は「任意値を使うな」と一見矛盾して見えるが、要点は値の大小ではなく「数字を直書きせず単一の命名トークン源から引く」こと。Material の dp 値は影のレンダリング指標であり z-index 値そのものではない(elevation と z-index は別管理)。isolation:isolate は IE 非対応だが 2024–2026 時点では実務上問題なし。コンテキスト生成プロパティの最終確認は MDN(primary)を正とすること。

blogWhat The Heck, z-index?? — Josh W. Comeau

primaryStacking context — MDN Web Docs

secondaryUnstacking CSS Stacking Contexts — Smashing Magazine

secondaryThe Value of z-index | CSS-Tricks

primaryOverview - Elevation - Atlassian Design

secondaryOutSystems UI Layer System: Managing z-index at scale

primaryElevation – Material Design 3

primaryElevation — Material Design (M2)

primaryZ-index | U.S. Web Design System (USWDS)

blogElevation Design Patterns: Tokens, Shadows, and Roles

blogThe 16 Ways CSS Creates Stacking Contexts — EdgeCases