overlayで重ねるレイアウトの落とし穴|z-indexとstacking contextを完全マスター
こんにちは!
今日は「overlayで要素を重ねるレイアウトで、なぜかz-indexが効かない!」という現場あるあるについて解説します。
僕が経験した「z-indexの呪い」
新人時代の僕は、z-index: 9999を書けば何でも前に来るんだと思ってました。
実装した画面を確認したら、モーダルの後ろに隠れてほしくない要素が前に出てるわ、z-indexを9999に上げても変わらんわで、ほんまにイライラしてたんですよ。
原因は、z-indexだけでは解決できない「stacking context」という概念を知らなかったからなんです。
現場でめっちゃ多いのが、このstacking contextを無視した実装なんですよ。
今では僕も後輩に説明するときに「z-indexは万能じゃない」ってよく言うんですけど、その理由がこのセクションの内容です。
stacking contextの正体を理解する
z-indexが効かない本当の理由は、stacking contextという親要素の階層構造にあります。
簡単に言うと、z-indexの比較は「同じ親を持つ要素同士」でしか成立しないってことなんですよ。
以下の実装を見てください。
<div class="container-a">
<div class="child" style="z-index: 100;">要素A</div>
</div>
<div class="container-b">
<div class="child" style="z-index: 1;">要素B</div>
</div>
この場合、要素Aのz-index: 100は要素Bのz-index: 1より大きいですけど、もし.container-bにz-index: 10があったら、要素Bが前に来ます。
なぜなら、z-indexの比較は「container-a内での100」と「container-b内での1」ではなく、「container-a全体」と「container-b全体」が比較されるからです。
これがstacking contextの正体なんですよ。
以下のプロパティがあると、新しいstacking contextが生まれます。
position: relative/absolute/fixed(かつz-indexがauto以外)display: flex/grid(かつz-indexがauto以外)opacityが1未満transformがあるfilterがあるmix-blend-modeがnormal以外
中でも引っかかりやすいのがopacityやtransformですね。
例えば、親要素にフェードイン効果でopacity: 0.8を付けてたら、その子要素のz-indexがいくら大きくても、別の要素グループには勝てないわけです。
overlayレイアウトで確実に効く実装パターン
じゃあ、実際にモーダルやtooltipなどのoverlayを作るときは、どう実装したらいいのか?
僕が現場で使ってる「確実に効く」パターンを紹介します。
パターン1:ボディ直下にoverlayを配置する
最もシンプルで確実な方法は、overlayを<body>の直下に配置することです。
<body>
<header>...</header>
<main>...</main>
<!-- overlayはbodyの直下に -->
<div class="modal-overlay">
<div class="modal-content">...</div>
</div>
</body>
CSSはこんな感じですね。
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
/* ここではz-indexは不要です */
}
この方法なら、他のページの構造が複雑でも、overlayが絶対に前に来ます。
パターン2:React等のポータルを使う
Reactを使ってる現場だったら、ReactDOM.createPortal()を使うといいですよ。
この方法は、overlayコンポーネントをDOM上ではbody直下に移動させるので、親要素のstacking contextに影響されません。
// Modal.jsx
import { createPortal } from 'react-dom';
export function Modal({ isOpen, children }) {
if (!isOpen) return null;
return createPortal(
<div className="modal-overlay">
<div className="modal-content">
{children}
</div>
</div>,
document.body
);
}
これめっちゃ便利で、親コンポーネントがどこに配置されてても、overlayは常にトップレベルで描画されます。
パターン3:複数のoverlayが必要な場合
モーダルの上にtooltipを出したい、みたいなケースもありますよね。
そういうときはz-indexの「層」を設定しておくといいですよ。
/* z-indexの層管理 */
.dropdown { z-index: 100; }
.tooltip { z-index: 200; }
.modal-overlay { z-index: 1000; }
.notification { z-index: 2000; }
.loading-overlay { z-index: 9999; }
あるいは、SCSSの変数で管理するのも良いです。
// _variables.scss
$z-dropdown: 100;
$z-tooltip: 200;
$z-modal: 1000;
$z-notification: 2000;
$z-loading: 9999;
.modal-overlay { z-index: $z-modal; }
こうしておくと、「あ、このnotificationがmodalの後ろに隠れてる