overlayで重ねるレイアウトの落とし穴|z-indexとstacking contextを完全マスター

C
クリオ
Web制作ディレクター / フロントエンジニア

こんにちは!

今日は「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-bz-index: 10があったら、要素Bが前に来ます。
なぜなら、z-indexの比較は「container-a内での100」と「container-b内での1」ではなく、「container-a全体」と「container-b全体」が比較されるからです。

これがstacking contextの正体なんですよ。
以下のプロパティがあると、新しいstacking contextが生まれます。

  • position: relative/absolute/fixed(かつz-indexauto以外)
  • display: flex/grid(かつz-indexauto以外)
  • opacityが1未満
  • transformがある
  • filterがある
  • mix-blend-modenormal以外

中でも引っかかりやすいのがopacitytransformですね。
例えば、親要素にフェードイン効果で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の後ろに隠れてる