結論
z-index が効かないとき、プロは数字を見ない——「この要素はどのスタッキングコンテキストに閉じ込められているか」を疑う。z-index は同じコンテキスト内の兄弟としか比較されないローカルな値なので、親の opacity:0.99 や transform が作ったコンテキストの中では z-index:999999 でも外には出られない。解は2つ——isolation:isolate で副作用ゼロのコンテキストを意図的に作り、すべての z-index は CSS変数の命名トークン階段から引く。そして「elevation(影=奥行きの見た目)」と「z-index(重なり順)」を別物として設計する。
01 — まず「どのフォルダに閉じ込められたか」を疑う
ブラウザはまず親コンテキスト(フォルダ)を並べ、その中で子(紙)を並べる。だから子は別フォルダの紙の間に割り込めない。tooltip:999999 が header:2 の下に潜るのは、親 main に position:relative; z-index:1 が付いてツールチップがローカルコンテキストに封印されているから。数字を上げる前に、効かない要素の祖先を DevTools で遡って opacity<1 / transform / position:fixed を探すのが第一手。
(opacity:.99)
(opacity:1)
blogWhat The Heck, z-index?? — Josh W. Comeau
primaryStacking context — MDN Web Docs
02 — コンテキストは isolation:isolate で意図的に作る
opacity:0.99 や transform:translateX(0)(実質no-op)でうっかりコンテキストを作るのをやめ、isolation:isolate を使う。これは「新しいスタッキングコンテキストを作る」だけの仕事をし、視覚的副作用ゼロ・position 不要・z-index値の指定も不要。コンポーネント内部の重なりをローカルに閉じ込められるので、外側の z-index 戦争に巻き込まれない再利用可能な部品になる。z-index 自体は !important と同じ「最後の手段」と捉える。
z:40
z:1
blogWhat The Heck, z-index?? — Josh W. Comeau
secondaryThe Value of z-index | CSS-Tricks
03 — 魔法の数字をやめ、命名トークンの階段から引く
999 や 10001 を直書きすると、後続が読めなくなり「z-index 軍拡競争」が始まる。少数・広間隔の命名トークンを :root に定義し、すべての z-index はそこを参照する。Atlassian の実ラダー(nav 200 → dropdown 300 → modal 510 → tooltip 800)は100刻みで、後から層を差し込める。値の大小は本質ではなく「単一の命名トークン源から引く」ことが肝。
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 と独立管理する。
(浮いてる風)
手前
shadow-1 / z-base
shadow-3 / z-popup
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 序列を完全に飛び越え最前面に出る。モーダル/トーストでラダーを管理する手間が減る方向。ただしレガシー対応やフォーカストラップは別途必要。
(headerの下)
top layer ↑
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.99 や transform の代わりにこれを使い、無意見で再利用可能な部品にする。
/* このカードは『フォルダ』になる:中の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 は別途)
限界 / 出典
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)