【CSS】z-indexが効かない原因と解決方法|スタッキングコンテキスト完全解説

【CSS】z-index が効かない原因と解決方法 HTML/CSS

CSSで要素の重なり順を制御するz-indexプロパティ。しかし、「z-indexを指定しているのに効かない」「z-index: 9999にしても上に来ない」という問題に悩まされた経験はありませんか?

z-indexが効かない原因は、単に値が小さいからではありません。スタッキングコンテキスト(Stacking Context)という概念を理解していないと、いくらz-indexの値を大きくしても問題は解決しません。

この記事では、z-indexが効かない7つの原因とそれぞれの解決方法を、コード例付きで徹底解説します。さらに、z-indexの設計ベストプラクティス、実装パターン集、DevToolsでのデバッグ方法まで網羅した完全ガイドです。

この記事で学べること

  • z-indexの基本的な仕組みとpositionプロパティとの関係
  • スタッキングコンテキスト(Stacking Context)の完全理解
  • z-indexが効かない7つの原因と具体的な解決方法
  • opacity / transform / filter がz-indexに与える影響
  • Flexbox / Grid の子要素でのz-indexの挙動
  • isolation プロパティによるスコープ制御
  • CSSフレームワークとのz-index競合の解決
  • position: fixed の特殊な挙動と注意点
  • z-index設計のベストプラクティスとレイヤー管理
  • モーダル・ドロップダウン・ツールチップの実装パターン
  • Chrome DevToolsでのデバッグ方法
スポンサーリンク
  1. z-indexとは?基本の使い方
    1. z-indexの仕組み(重ね順の制御)
    2. z-indexの基本構文と値の範囲
    3. positionプロパティとの関係(static以外が必要)
    4. 自然な重ね順(HTMLの後に書いたものが上)
  2. 原因①:positionが指定されていない
    1. 問題のコード
    2. なぜ効かないのか
    3. 解決方法
    4. position: relativeはレイアウトに影響しない
    5. 各positionの使い分けガイド
    6. よくある間違いパターン
  3. 原因②:スタッキングコンテキスト(Stacking Context)
    1. スタッキングコンテキストとは何か
    2. 具体例で理解する
    3. 解決方法
    4. 新しいスタッキングコンテキストが作られる条件一覧
    5. 異なるスタッキングコンテキスト間の重ね順
  4. 原因③:opacity / transform / filter が原因
    1. opacity < 1 でスタッキングコンテキストが作られる
    2. transform でスタッキングコンテキストが作られる
    3. filter / backdrop-filter でも作られる
    4. will-change プロパティの影響
    5. contain: paint / layout の影響
  5. 原因④:Flexbox / Grid の子要素
    1. Flexboxの子要素ではpositionなしでz-indexが効く
    2. Gridの子要素も同様
    3. 注意:スタッキングコンテキストが作られる
  6. 原因⑤:isolation プロパティ
    1. isolation: isolate の仕組み
    2. z-index問題を局所化するテクニック
    3. Reactコンポーネントでの活用例
  7. 原因⑥:CSSフレームワーク・ライブラリとの競合
    1. Bootstrap の z-index 体系
    2. Tailwind CSS の z-index ユーティリティ
    3. z-indexのインフレ問題(z-index: 99999 の罠)
  8. 原因⑦:position: fixed の特殊な挙動
    1. fixed要素もスタッキングコンテキストの影響を受ける
    2. transformを持つ親がいるとfixedの基準が変わる
    3. fixed要素同士のz-index
    4. fixedの基準が変わるプロパティ一覧
  9. z-index 設計のベストプラクティス
    1. z-indexのスケール設計(CSS変数で管理)
    2. レイヤー分けの考え方
    3. isolation: isolate でスコープを区切る
    4. z-indexのコードレビューチェックリスト
  10. 実装パターン集
    1. パターン1: モーダル / オーバーレイ
    2. パターン2: ドロップダウンメニュー
    3. パターン3: ツールチップ
    4. パターン4: stickyヘッダー + ドロップダウン
    5. パターン5: 画像のオーバーラップレイアウト
  11. DevTools でのデバッグ方法
    1. Chrome DevTools の 3D View(Layers パネル)
    2. Computed タブで z-index の計算値を確認
    3. スタッキングコンテキストの特定方法
  12. まとめ
    1. z-indexが効かない原因チェックリスト
    2. スタッキングコンテキストが作られる条件一覧表(再掲)
    3. z-index設計テンプレート

z-indexとは?基本の使い方

まず、z-indexプロパティの基本を確認しましょう。z-indexは、要素の重なり順(Z軸方向の位置)を制御するCSSプロパティです。

z-indexの仕組み(重ね順の制御)

Webページは基本的に2次元(X軸とY軸)でレイアウトされますが、要素が重なる場合にはZ軸(画面に対して垂直方向)の概念が必要になります。z-indexはこのZ軸上での要素の順序を整数値で指定します。

値が大きいほど手前(ユーザーに近い側)に表示され、値が小さいほど奥に表示されます。

z-indexの基本的な使い方
/* z-indexの値が大きい要素が手前に表示される */
.back {
  position: relative;
  z-index: 1;   /* 奥に表示 */
}

.front {
  position: relative;
  z-index: 10;  /* 手前に表示 */
}

上記の例では、.back.frontの両方にposition: relativeが指定されており、z-indexの値で.front(z-index: 10)が.back(z-index: 1)より手前に表示されます。

z-indexの基本構文と値の範囲

z-indexプロパティは以下の値を取ることができます。

z-indexの構文
/* 基本構文 */
z-index: auto;      /* デフォルト値 */
z-index: 0;         /* 整数値 0 */
z-index: 1;         /* 正の整数 */
z-index: 100;       /* 正の整数 */
z-index: -1;        /* 負の整数 */
z-index: inherit;   /* 親要素から継承 */
z-index: initial;   /* 初期値(auto)にリセット */
説明 スタッキングコンテキスト作成
autoデフォルト値。親要素と同じスタッキング順序作成しない
正の整数値が大きいほど手前に表示作成する(positioned要素のみ)
0明示的に0を指定(autoとは異なる)作成する(positioned要素のみ)
負の整数親要素の背景より奥に配置可能作成する(positioned要素のみ)

注意:z-index: autoz-index: 0は見た目上の重ね順は同じですが、大きな違いがあります。z-index: 0は新しいスタッキングコンテキストを作成しますが、z-index: autoは作成しません。この違いは後述のスタッキングコンテキストの解説で詳しく説明します。

positionプロパティとの関係(static以外が必要)

z-indexが機能するための最も重要な前提条件は、要素にpositionプロパティがstatic以外で指定されていることです。これがz-indexが効かない最も一般的な原因です。

positionの値 z-indexが効くか 備考
static(デフォルト)❌ 効かないz-indexは完全に無視される
relative✅ 効くレイアウトに影響しない(最も安全)
absolute✅ 効く通常フローから外れる
fixed✅ 効くビューポート基準で固定
sticky✅ 効くスクロール位置で切替

自然な重ね順(HTMLの後に書いたものが上)

z-indexを指定しない場合、要素の重なり順はCSS仕様の「ペインティング順序(Painting Order)」に従います。下から上の順に並べると以下のようになります。

順序 要素の種類 説明
1(最も奥)ルート要素の背景・ボーダーhtml要素のスタイル
2z-indexが負の値のpositioned要素z-index: -1 など
3通常フローのブロック要素div, p, section など
4フロート要素float: left / right
5通常フローのインライン要素span, a, テキスト
6z-index: auto/0 のpositioned要素positionが指定された要素
7(最も手前)z-indexが正の値のpositioned要素z-index: 1 以上

同じレベルの要素同士では、HTMLソースで後に記述された要素が手前に表示されます。

自然な重ね順の例
<!-- HTMLの記述順で重なりが決まる -->
<div class="box box-1">BOX 1(奥)</div>
<div class="box box-2">BOX 2(手前)</div>
CSS
.box {
  position: absolute;
  width: 200px;
  height: 200px;
}
.box-1 { background: #3b82f6; top: 20px; left: 20px; }
.box-2 { background: #ef4444; top: 60px; left: 60px; }
/* box-2が後に書かれているため、自然に手前に表示される */

ポイント:z-indexが不要な場面も多いです。HTMLの記述順だけで解決できる場合は、余計なz-indexを追加しないことがベストプラクティスです。

原因①:positionが指定されていない

z-indexが効かない最も多い原因がこれです。z-indexは、positionプロパティがstatic以外(relativeabsolutefixedsticky)に設定された要素にのみ適用されます。

問題のコード

以下のコードでは、z-indexを指定していますが全く効きません。

HTML
<div class="content">コンテンツ</div>
<div class="overlay">オーバーレイ</div>
❌ z-indexが効かないCSS
.content {
  position: relative;
  z-index: 10;
  background: #fff;
}

.overlay {
  /* positionが指定されていない(デフォルトはstatic) */
  z-index: 9999;  /* ← いくら大きくしても効かない! */
  background: rgba(0, 0, 0, 0.5);
  width: 100%;
  height: 100%;
}

なぜ効かないのか

CSS仕様(CSS Positioned Layout Module Level 3)では、z-indexはpositioned要素(positionがstatic以外の要素)にのみ適用されると定義されています。position: staticの要素は通常フロー内にあり、Z軸方向の位置決めの対象外です。

DevToolsのComputedタブを確認すると、position: staticの要素に指定したz-indexの計算値はautoになっており、指定が完全に無視されていることが分かります。

解決方法

✅ position: relative を追加して解決
.overlay {
  position: relative;  /* ← これを追加するだけ! */
  z-index: 9999;
  background: rgba(0, 0, 0, 0.5);
  width: 100%;
  height: 100%;
}

position: relativeはレイアウトに影響しない

position: relativeを追加しても、toprightbottomleftを指定しない限り要素の位置は変わりません。z-indexだけを効かせたい場合に最も安全な選択肢です。

レイアウトに影響なくz-indexを有効にする
.header {
  position: relative;  /* レイアウトに影響なし */
  z-index: 100;        /* z-indexが有効になる */
  /* top, left等は指定しない → 位置は変わらない */
}

.dropdown-menu {
  position: absolute; /* 親を基準に配置 */
  z-index: 200;
  top: 100%;
  left: 0;
}

各positionの使い分けガイド

positionの値 主な用途 特徴
relativez-indexだけ効かせたい最も安全。レイアウトに影響なし
absolute親要素を基準に配置通常フローから外れる
fixed画面に固定表示ビューポート基準
stickyスクロール時に固定relative/fixedを切替

よくある間違いパターン

❌ パターン1: ナビゲーションの親にpositionがない
.nav-item {
  /* position: relative; を忘れている */
  z-index: 100;  /* ← 効かない */
}

/* ✅ 修正 */
.nav-item {
  position: relative;
  z-index: 100;
}
❌ パターン2: モーダルのオーバーレイ
.modal-backdrop {
  z-index: 1000;
  background: rgba(0,0,0,0.5);
  width: 100vw;
  height: 100vh;
  top: 0; left: 0;
  /* ← position: fixed; を忘れている! */
}

/* ✅ 修正 */
.modal-backdrop {
  position: fixed;
  z-index: 1000;
  background: rgba(0,0,0,0.5);
  inset: 0;
}
❌ パターン3: CSSリセットで上書きされている
/* 極端なリセットCSS */
* { position: static; }

/* CSSの詳細度で負けている場合 */
#wrapper .content .overlay {
  position: static;  /* ← 詳細度が高く上書きされる */
}
.overlay {
  position: relative; /* ← 詳細度が低いので効かない */
  z-index: 999;
}

ポイント:z-indexが効かないと思ったら、まずDevToolsでその要素のpositionの計算値を確認しましょう。staticになっていたら、position: relativeを追加するだけで解決します。

原因②:スタッキングコンテキスト(Stacking Context)

z-indexが効かない原因で最も理解が難しく、最も重要な概念がスタッキングコンテキスト(Stacking Context)です。この概念を理解しないと、z-indexの問題は永遠に解決できません。

スタッキングコンテキストとは何か

スタッキングコンテキストとは、要素の重なり順を決める独立したグループのことです。z-indexの値は、同じスタッキングコンテキスト内の要素同士でのみ比較されます。

例えるなら、スタッキングコンテキストは「書類のフォルダ」のようなものです。フォルダA(z-index: 1)の中に100ページの書類があっても、フォルダB(z-index: 2)が上に積まれたら、フォルダAの全ページはフォルダBの下になります。

スタッキングコンテキストの例え

  • フォルダA(z-index: 1)の中にz-index: 9999のページがある
  • フォルダB(z-index: 2)の中にz-index: 1のページがある
  • フォルダBがフォルダAの上にあるため、フォルダBの全ページがフォルダAの全ページより上
  • つまり、フォルダAの9999ページ目よりフォルダBの1ページ目が上にくる
スタッキングコンテキストの概念図
ルート スタッキングコンテキスト(html要素)
┌────────────────────────────────────────────┐
│                                              │
│  ┌──────────────────┐  ┌──────────────────┐ │
│  │ コンテキストA       │  │ コンテキストB       │ │
│  │ (z-index: 1)      │  │ (z-index: 2)      │ │
│  │                   │  │                   │ │
│  │  子A: z-index:   │  │  子B: z-index:   │ │
│  │  9999            │  │  1              │ │
│  │                   │  │                   │ │
│  └──────────────────┘  └──────────────────┘ │
│                                              │
│  結果: 子B(1) が 子A(9999) より手前に表示!     │
│  理由: 親A(1) < 親B(2) だから              │
└────────────────────────────────────────────┘

具体例で理解する

HTML
<div class="parent-a">
  <div class="child-a">z-index: 9999</div>
</div>
<div class="parent-b">
  <div class="child-b">z-index: 1</div>
</div>
❌ z-index: 9999でも上に来ないCSS
.parent-a {
  position: relative;
  z-index: 1;    /* ← スタッキングコンテキスト作成 */
}
.child-a {
  position: absolute;
  z-index: 9999; /* ← parent-aの中でしか比較されない */
  background: #3b82f6;
  padding: 20px;
}

.parent-b {
  position: relative;
  z-index: 2;    /* ← parent-a(1)より大きい */
}
.child-b {
  position: absolute;
  z-index: 1;    /* ← たった1でもchild-aより手前 */
  background: #ef4444;
  padding: 20px;
}

結果:child-b(z-index: 1)がchild-a(z-index: 9999)より手前に表示されます。parent-a(z-index: 1)< parent-b(z-index: 2)なので、parent-a内の全要素はparent-b内の全要素より奥に配置されます。

解決方法

✅ 解決方法1: 親のz-indexを調整
.parent-a {
  position: relative;
  z-index: 3;  /* ← parent-b(2)より大きくする */
}
✅ 解決方法2: 親のスタッキングコンテキストを解除
.parent-a {
  position: relative;
  /* z-indexを指定しない(auto)
     → スタッキングコンテキストが作られない */
}
.parent-b {
  position: relative;
  /* z-indexを指定しない(auto) */
}
/* 両方autoならchild-aとchild-bはルートコンテキストで直接比較
   → child-a(9999) > child-b(1) でchild-aが手前 */

重要:z-index: autoz-index: 0の違い。見た目の重ね順は同じですが、z-index: 0は新しいスタッキングコンテキストを作成し、z-index: autoは作成しません。これがz-index問題の核心です。

新しいスタッキングコンテキストが作られる条件一覧

以下の条件のいずれかを満たす要素は、新しいスタッキングコンテキストを作成します。

条件 頻度
ルート要素(html)常にルートコンテキスト
position: absolute/relative + z-indexがauto以外position: relative; z-index: 1;★★★
position: fixed / stickyposition: fixed;★★★
Flex/Gridの子 + z-indexがauto以外親がdisplay: flexで子にz-index: 1★★
opacityが1未満opacity: 0.99;★★★
transformがnone以外transform: translateZ(0);★★★
filterがnone以外filter: blur(0);★★
backdrop-filterがnone以外backdrop-filter: blur(10px);★★
perspectiveがnone以外perspective: 1000px;
clip-pathがnone以外clip-path: circle(50%);
mask / mask-imageがnone以外mask-image: url(...);
mix-blend-modeがnormal以外mix-blend-mode: multiply;
isolation: isolateisolation: isolate;★★
will-changeで上記プロパティを指定will-change: transform;★★
contain: layout / paint / strict / contentcontain: paint;

★が多いほど実際の開発でよく遭遇します。特にposition + z-indexopacitytransformは最も高頻度で問題の原因となります。

異なるスタッキングコンテキスト間の重ね順

異なるスタッキングコンテキストに属する要素同士の重ね順は、以下の手順で決まります。

重ね順の決定アルゴリズム
Step 1: 要素Aと要素Bが同じスタッキングコンテキスト内か確認
  → 同じなら z-index の値を直接比較

Step 2: 異なるコンテキストの場合、共通の祖先コンテキストを探す
  → 共通の祖先コンテキスト内で、
     それぞれの親コンテキストのz-indexを比較

Step 3: 親コンテキストのz-indexが大きい方の子孫が手前

重要: 子要素のz-indexがいくら大きくても、
親コンテキストのz-indexを超えることはできない
複雑なネストの例
/* 3階層のスタッキングコンテキスト */
.grandparent {
  position: relative;
  z-index: 1;  /* コンテキスト作成 */
}

.parent {
  position: relative;
  z-index: 10; /* grandparent内でのコンテキスト */
}

.child {
  position: absolute;
  z-index: 9999; /* parent内でのみ有効 */
}

/*
  .childの実効的な重ね順:
  grandparent(1) → parent(10) → child(9999)

  外部の要素と比較するときは
  grandparentのz-index: 1が使われる
*/

原因③:opacity / transform / filter が原因

z-indexの問題でよくある原因が、opacity、transform、filterなどのCSSプロパティです。これらは意図せず新しいスタッキングコンテキストを作成してしまいます。

opacity < 1 でスタッキングコンテキストが作られる

opacityの値が1未満の場合、その要素は新しいスタッキングコンテキストを作成します。フェードイン・フェードアウトのアニメーションで頻繁に問題になります。

HTML
<div class="card">
  <div class="tooltip">ツールチップ</div>
  カード内容
</div>
<div class="next-card">次のカード</div>
❌ opacityが原因でz-indexが効かない
.card {
  position: relative;
  opacity: 0.95;  /* ← スタッキングコンテキストが作られる! */
}
.tooltip {
  position: absolute;
  z-index: 9999;  /* ← .cardのコンテキスト内に閉じ込められる */
  bottom: 100%;
  background: #1e293b;
  color: #fff;
  padding: 8px 12px;
  border-radius: 6px;
  white-space: nowrap;
}
.next-card {
  position: relative;
  z-index: 1;
  /* ← .tooltip(9999)より上に表示されてしまう! */
}

なぜ効かないのか:.cardopacity: 0.95が指定されているため新しいスタッキングコンテキストが作成されます。.tooltipのz-index: 9999は.card内でのみ有効で、外にある.next-cardとは直接比較できません。

✅ 解決方法
/* 解決方法1: opacityを削除または1にする */
.card {
  position: relative;
  opacity: 1;
}

/* 解決方法2: 背景色のalpha値で代替する */
.card {
  position: relative;
  background: rgba(255, 255, 255, 0.95);
  /* opacityではなくbackground-colorのalphaを使う
     → スタッキングコンテキストが作られない */
}

/* 解決方法3: ツールチップをDOM構造で.cardの外に出す */

transform でスタッキングコンテキストが作られる

transformプロパティにnone以外の値を設定すると、新しいスタッキングコンテキストが作成されます。GPUアクセラレーションのためにtransform: translateZ(0)を追加するケースでよく問題になります。

❌ transformが原因の例
.sidebar {
  position: relative;
  transform: translateZ(0);  /* GPU高速化のつもりが... */
}
.sidebar .dropdown {
  position: absolute;
  z-index: 1000;
  /* .sidebarのコンテキスト内に閉じ込められる */
}
✅ 解決方法
/* transformを削除する */
.sidebar {
  position: relative;
  /* transformを削除 */
}

/* またはドロップダウンを.sidebarの外に配置 */

特に注意が必要なtransform値の一覧です。

スタッキングコンテキストを作るtransformの例
/* 全てスタッキングコンテキストを作る */
transform: translateZ(0);
transform: translate3d(0, 0, 0);
transform: scale(1);
transform: rotate(0deg);
transform: translateX(0);

/* スタッキングコンテキストを作らない */
transform: none;  /* ← これだけ作らない */

filter / backdrop-filter でも作られる

filterbackdrop-filternone以外で新しいスタッキングコンテキストを作成します。

❌ filterが原因の例
.image-wrapper {
  position: relative;
  filter: drop-shadow(0 4px 6px rgba(0,0,0,0.1));
  /* ← スタッキングコンテキストが作られる */
}
.image-wrapper .badge {
  position: absolute;
  z-index: 100;
  /* 外の要素より上に来ない可能性 */
}

/* ✅ 解決: filter: drop-shadow()の代わりにbox-shadowを使う */
.image-wrapper {
  position: relative;
  box-shadow: 0 4px 6px rgba(0,0,0,0.1);
  /* box-shadowはスタッキングコンテキストを作らない */
}

will-change プロパティの影響

will-changeopacitytransformfilter等を指定すると、実際にそのプロパティが変更されていなくても新しいスタッキングコンテキストが作成されます。

will-changeの注意点
/* ❌ 常時指定するとコンテキストが作られっぱなし */
.container {
  will-change: transform;
  /* transform自体は指定していなくてもコンテキストが作られる */
}

/* ✅ 必要な時だけ適用 */
.container {
  will-change: auto;
}
.container:hover {
  will-change: transform;
}

contain: paint / layout の影響

containプロパティのpaintlayoutも新しいスタッキングコンテキストを作成します。

containの挙動
/* スタッキングコンテキストを作る */
contain: paint;     /* ← 作る */
contain: layout;    /* ← 作る */
contain: strict;    /* ← 作る(layout + paint + size) */
contain: content;   /* ← 作る(layout + paint) */

/* スタッキングコンテキストを作らない */
contain: size;      /* ← 作らない */
contain: style;     /* ← 作らない */

opacity / transform / filter 問題の解決早見表

  • opacity < 1 → 背景色のalpha値で代替する
  • transform: translateZ(0) → 本当に必要か再検討、不要なら削除
  • filter: drop-shadow()box-shadowで代替
  • will-change: transform → hover時のみ適用する
  • contain: paintcontain: sizeのみにする
  • 上記が削除できない場合 → ポップアップ要素をDOM構造で外に出す

原因④:Flexbox / Grid の子要素

CSS FlexboxとGridレイアウトの子要素には、positionを指定しなくてもz-indexが効くという特殊な挙動があります。便利な反面、意図しないスタッキングコンテキストの原因にもなります。

Flexboxの子要素ではpositionなしでz-indexが効く

HTML
<div class="flex-container">
  <div class="item-a">A</div>
  <div class="item-b">B</div>
</div>
Flex子要素でz-indexが効く
.flex-container {
  display: flex;
}
.item-a {
  /* positionを指定していない! */
  z-index: 2;  /* ← それでも効く */
  background: #3b82f6;
  margin-right: -20px; /* 重なりを作る */
}
.item-b {
  z-index: 1;
  background: #ef4444;
}

CSS Flexbox仕様で、Flexアイテムはposition: staticでも、z-indexがauto以外の場合に新しいスタッキングコンテキストを作成すると明記されています。

Gridの子要素も同様

Grid子要素でのz-index
.grid-container {
  display: grid;
  grid-template-columns: 1fr 1fr;
}
.grid-item-1 {
  grid-column: 1 / 3;  /* 両方の列にまたがる */
  grid-row: 1;
  z-index: 1;  /* positionなしでOK */
}
.grid-item-2 {
  grid-column: 1 / 2;
  grid-row: 1;  /* item-1と重なる */
  z-index: 2;  /* 手前に表示 */
}

注意:スタッキングコンテキストが作られる

Flexbox/Gridの子要素にz-indexを指定すると、その子要素は新しいスタッキングコンテキストを作成します。子要素の中の孫要素のz-indexは外の要素と直接比較できなくなります。

❌ Flex子要素のコンテキストで問題が起きる
.flex-container { display: flex; }

.card {
  z-index: 1;  /* ← コンテキストが作られる */
}
.card .dropdown {
  position: absolute;
  z-index: 9999;
  /* 隣の.cardの上に表示されない可能性 */
}
✅ 解決方法: ホバー時のみz-indexを上げる
.flex-container { display: flex; }

.card {
  position: relative;
  /* z-indexを指定しない */
}
/* ドロップダウン表示時のみz-indexを上げる */
.card:hover,
.card.is-open {
  z-index: 10;
}
.card .dropdown {
  position: absolute;
  z-index: 100;
}

ポイント:Flexbox/Gridの子要素ではpositionなしでもz-indexが効きますが、同時にスタッキングコンテキストが作られます。不要なz-indexは指定しないのがベストです。

原因⑤:isolation プロパティ

isolationプロパティは、意図的にスタッキングコンテキストを作成するためのプロパティです。これ自体がz-indexの問題を引き起こすこともあれば、問題を解決するために使うこともできます。

isolation: isolate の仕組み

isolation: isolateを指定すると、その要素は新しいスタッキングコンテキストを作成します。z-indexを指定する必要がないため、他の要素との重なり順に影響を与えずにコンテキストを作成できます。

isolation: isolate の基本
.component {
  isolation: isolate;
  /*
    これだけで新しいスタッキングコンテキストが作られる
    - positionの変更不要
    - z-indexの指定不要
    - レイアウトへの影響なし
  */
}

/* コンポーネント内のz-indexは外に漏れない */
.component .inner-element {
  position: relative;
  z-index: 9999;
  /* ← .componentのコンテキスト内に閉じ込められる */
}

z-index問題を局所化するテクニック

isolation: isolateは、コンポーネント内のz-indexが外部に影響しないようにスコープを区切るのに最適です。

❌ isolationなし: z-indexが外に漏れる
<!-- カードコンポーネント -->
<div class="card">
  <div class="card-image" style="position:relative; z-index:1"></div>
  <div class="card-badge" style="position:relative; z-index:2"></div>
</div>

<!-- card-badgeのz-index:2が外部の要素にも影響してしまう -->
✅ isolationでスコープを区切る
.card {
  isolation: isolate;
  /*
    カード内のz-indexはカード外に影響しない
    他のコンポーネントとのz-index競合を防げる
  */
}

Reactコンポーネントでの活用例

コンポーネントのルートにisolation: isolate
/* コンポーネントのルート要素に適用 */
.header { isolation: isolate; }
.sidebar { isolation: isolate; }
.main-content { isolation: isolate; }
.footer { isolation: isolate; }

/*
  各コンポーネント内のz-indexは
  そのコンポーネント内でのみ有効
  → z-indexの競合が起きにくい
*/

ポイント:isolation: isolateは「z-indexのスコープを作る」最もクリーンな方法です。コンポーネント設計時に積極的に活用しましょう。ただし、コンポーネント内のポップアップが外に出られなくなる点には注意が必要です。

原因⑥:CSSフレームワーク・ライブラリとの競合

Bootstrap、Tailwind CSS、Material UIなどのCSSフレームワークは、独自のz-index体系を持っています。自分のCSSとフレームワークのz-indexが競合すると、想定外の重なり順になることがあります。

Bootstrap の z-index 体系

Bootstrap 5では以下のz-index値が使われています。

コンポーネント z-index値 CSS変数
Dropdown1000$zindex-dropdown
Sticky1020$zindex-sticky
Fixed1030$zindex-fixed
Offcanvas backdrop1040$zindex-offcanvas-backdrop
Offcanvas1045$zindex-offcanvas
Modal backdrop1050$zindex-modal-backdrop
Modal1055$zindex-modal
Popover1070$zindex-popover
Tooltip1080$zindex-tooltip
Toast1090$zindex-toast

Tailwind CSS の z-index ユーティリティ

Tailwind CSSのz-indexクラス
/* Tailwind CSS デフォルトのz-indexユーティリティ */
.z-0    { z-index: 0; }
.z-10   { z-index: 10; }
.z-20   { z-index: 20; }
.z-30   { z-index: 30; }
.z-40   { z-index: 40; }
.z-50   { z-index: 50; }
.z-auto { z-index: auto; }

/* カスタム値も使える */
/* <div class="z-[100]"> → z-index: 100 */

z-indexのインフレ問題(z-index: 99999 の罠)

z-indexの問題が発生すると、つい値を大きくして解決しようとしがちです。しかし、これは「z-indexインフレ」と呼ばれるアンチパターンです。

❌ z-indexインフレの典型例
/* 開発者A: ヘッダーを最前面に */
.header { z-index: 100; }

/* 開発者B: モーダルをヘッダーより上に */
.modal { z-index: 999; }

/* 開発者C: ツールチップをモーダルより上に */
.tooltip { z-index: 9999; }

/* 開発者D: 緊急通知を最前面に */
.emergency-banner { z-index: 99999; }

/* 開発者E: デバッグパネルをさらに上に... */
.debug-panel { z-index: 999999; }

/* ↑ このインフレは終わりがない... */

z-indexの値を大きくしても、スタッキングコンテキストの問題は解決しません。根本的な原因を理解し、適切なレイヤー設計を行うことが重要です。

注意:z-index: 99999 のような巨大な値が必要になった時点で、設計を見直すべきサインです。後述のベストプラクティスでレイヤー設計の方法を解説します。

原因⑦:position: fixed の特殊な挙動

position: fixedは常にビューポートを基準に配置されると思われがちですが、実はスタッキングコンテキストの影響を受けます。さらに、親要素にtransformがある場合は基準点自体が変わるという特殊な挙動があります。

fixed要素もスタッキングコンテキストの影響を受ける

position: fixedの要素は自動的にスタッキングコンテキストを作成します。しかし、親要素のスタッキングコンテキスト内に閉じ込められる場合があります。

❌ fixed要素がスタッキングコンテキストに閉じ込められる
.sidebar {
  position: relative;
  z-index: 1;  /* コンテキスト作成 */
}

.sidebar .fixed-button {
  position: fixed;
  bottom: 20px;
  right: 20px;
  z-index: 9999;
  /* ← .sidebar(z-index:1)のコンテキスト内
     外部のz-index:2以上の要素に負ける */
}

.main-content {
  position: relative;
  z-index: 2;
  /* ← .fixed-buttonより手前に表示される */
}

transformを持つ親がいるとfixedの基準が変わる

これは特に厄介な挙動です。親要素にtransformnone以外)が指定されていると、position: fixedの基準がビューポートではなく、その親要素に変わります。

❌ transformがfixedの基準を変える
.animated-section {
  transform: translateY(0);
  /* または will-change: transform; でも同様 */
}

.animated-section .fixed-header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  /*
    期待: ビューポート上端に固定
    実際: .animated-sectionを基準に配置される!
    スクロールしてもビューポートに固定されない
  */
}
✅ 解決方法
/* 解決方法1: fixed要素をtransformを持つ親の外に移動 */
<body>
  <div class="fixed-header">...</div>  <!-- transformの外に配置 -->
  <div class="animated-section">...</div>
</body>

/* 解決方法2: 親のtransformを削除できる場合は削除 */
.animated-section {
  transform: none;
}

fixed要素同士のz-index

position: fixed同士の要素は、同じルートスタッキングコンテキスト内にいる場合はz-indexの値で直接比較されます。

fixed要素同士の重ね順
/* fixed要素同士はz-indexで直接比較 */
.fixed-header {
  position: fixed;
  top: 0;
  z-index: 100;   /* ヘッダー */
}

.fixed-modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 200;   /* モーダル背景(ヘッダーより手前) */
}

.fixed-modal {
  position: fixed;
  z-index: 300;   /* モーダル本体(背景より手前) */
}

.fixed-toast {
  position: fixed;
  top: 20px;
  right: 20px;
  z-index: 400;   /* トースト通知(最前面) */
}

ポイント:position: fixedを使う場合は、祖先要素にtransformfilterperspectiveが設定されていないか確認しましょう。これらが設定されていると、fixedの挙動が完全に変わります。

fixedの基準が変わるプロパティ一覧

以下のプロパティが祖先要素に設定されていると、position: fixedの基準がビューポートからその祖先要素に変わります。

プロパティ fixedの基準が変わる条件
transformnone以外のすべての値
perspectivenone以外の値
filternone以外の値
backdrop-filternone以外の値
will-changetransform, perspective, filterを指定
containpaint を含む値

z-index 設計のベストプラクティス

z-indexの問題を未然に防ぐためには、プロジェクト全体でz-indexのルールを統一することが重要です。ここでは、実務で使えるベストプラクティスを紹介します。

z-indexのスケール設計(CSS変数で管理)

プロジェクト全体でz-indexの値をCSS変数(カスタムプロパティ)で一元管理することで、z-indexのインフレを防げます。

CSS変数でz-indexを管理する
:root {
  /* z-index レイヤー定義 */
  --z-below: -1;        /* 背景の装飾 */
  --z-default: 0;       /* 通常レベル */
  --z-raised: 1;        /* 少し手前 */
  --z-dropdown: 10;     /* ドロップダウン */
  --z-sticky: 20;       /* スティッキーヘッダー */
  --z-overlay: 30;      /* オーバーレイ背景 */
  --z-modal: 40;        /* モーダル */
  --z-popover: 50;      /* ポップオーバー */
  --z-tooltip: 60;      /* ツールチップ */
  --z-toast: 70;        /* トースト通知 */
  --z-max: 100;         /* 最大値(緊急時のみ) */
}

/* 使用例 */
.header {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
}
.dropdown-menu {
  z-index: var(--z-dropdown);
}
.modal-backdrop {
  z-index: var(--z-overlay);
}
.modal {
  z-index: var(--z-modal);
}

レイヤー分けの考え方

レイヤー z-index範囲 用途
Background-1背景装飾背景パターン、装飾的画像
Content0 – 1通常コンテンツテキスト、画像、カード
Navigation10 – 20ナビゲーションドロップダウン、スティッキーヘッダー
Overlay30オーバーレイ背景モーダル背景、ドロワー背景
Modal40モーダル/ダイアログモーダル、確認ダイアログ
Popover50 – 60ポップオーバーツールチップ、ポップオーバー
Notification70通知トースト通知、アラート
Emergency100緊急時のみエラーオーバーレイ

isolation: isolate でスコープを区切る

コンポーネントごとにスコープを作る
/* コンポーネントのルートでisolationを使う */
.card-component {
  isolation: isolate;
  /* コンポーネント内のz-indexは外に漏れない */
}

/* コンポーネント内では自由にz-indexを使える */
.card-component .image { z-index: 1; }
.card-component .badge { z-index: 2; }
.card-component .overlay { z-index: 3; }

z-indexのコードレビューチェックリスト

z-index コードレビューチェックリスト

  • ☐ CSS変数(--z-xxx)を使っているか? マジックナンバーになっていないか?
  • positionが適切に指定されているか?
  • ☐ 親要素のスタッキングコンテキストを確認したか?
  • ☐ z-indexの値は最小限か?(9999のような値は使っていないか?)
  • isolation: isolateでスコープを区切れないか?
  • ☐ 他のコンポーネントのz-indexと競合しないか?
  • opacitytransformfilterがスタッキングコンテキストを作っていないか?
  • ☐ モバイル表示でもz-indexが正しく動作するか?

実装パターン集

ここでは、z-indexが必要になる典型的なUIパターンとその実装方法を紹介します。コピー&ペーストで使えるコード例付きです。

パターン1: モーダル / オーバーレイ

モーダルは最も一般的なz-indexの使用例です。オーバーレイ(半透明背景)とモーダル本体の2層構造が基本です。

HTML – モーダル構造
<!-- ページのコンテンツ -->
<main class="page-content">...</main>

<!-- モーダル(body直下に配置するのがベスト) -->
<div class="modal-backdrop" id="modalBackdrop"></div>
<div class="modal" id="modal">
  <div class="modal-content">
    <h2>モーダルタイトル</h2>
    <p>モーダルの内容...</p>
    <button class="modal-close">閉じる</button>
  </div>
</div>
CSS – モーダルのz-index設計
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  z-index: var(--z-overlay, 30);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s, visibility 0.3s;
}

.modal-backdrop.is-active {
  opacity: 1;
  visibility: visible;
}

.modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: var(--z-modal, 40);
  background: #fff;
  border-radius: 12px;
  padding: 2em;
  max-width: 500px;
  width: 90%;
  box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}

ポイント:モーダルはbody直下に配置するのがベストです。深いDOM構造の中に配置すると、祖先要素のスタッキングコンテキストに閉じ込められるリスクがあります。ReactではPortal、VueではTeleportを使いましょう。

パターン2: ドロップダウンメニュー

ドロップダウンメニューの実装
/* HTML */
<nav class="nav">
  <div class="nav-item">
    <button class="nav-link">メニュー</button>
    <div class="dropdown">
      <a href="#">サブメニュー1</a>
      <a href="#">サブメニュー2</a>
    </div>
  </div>
</nav>
CSS
.nav {
  position: relative;
  z-index: var(--z-sticky, 20);
}
.nav-item {
  position: relative;
  /* z-indexを指定しない → コンテキストを作らない */
}
.dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  min-width: 200px;
  background: #fff;
  border-radius: 8px;
  box-shadow: 0 10px 30px rgba(0,0,0,0.15);
  z-index: var(--z-dropdown, 10);
  display: none;
}
.nav-item:hover .dropdown {
  display: block;
}

パターン3: ツールチップ

CSS-onlyツールチップ
.tooltip-trigger {
  position: relative;
  cursor: help;
}

.tooltip-trigger::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  background: #1e293b;
  color: #fff;
  padding: 6px 12px;
  border-radius: 6px;
  font-size: 13px;
  white-space: nowrap;
  z-index: var(--z-tooltip, 60);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s;
}

.tooltip-trigger:hover::after {
  opacity: 1;
}

パターン4: stickyヘッダー + ドロップダウン

stickyヘッダーのドロップダウンがコンテンツの下に隠れる問題は、最もよくあるz-index問題の一つです。

stickyヘッダーの正しいz-index設計
.sticky-header {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky, 20);
  background: #fff;
  /*
    重要: position: sticky 自体が
    スタッキングコンテキストを作る
    ドロップダウンはこのコンテキスト内
  */
}

.main-content {
  position: relative;
  /* z-indexを指定しない(auto)
     → ヘッダー(z-index:20)より奥になる */
}

パターン5: 画像のオーバーラップレイアウト

Gridで画像をオーバーラップさせる
/* HTML */
<div class="overlap-grid">
  <img class="img-back" src="back.jpg" alt="">
  <img class="img-front" src="front.jpg" alt="">
</div>
CSS
.overlap-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0;
  max-width: 600px;
}
.img-back {
  grid-column: 1 / 2;
  grid-row: 1;
  width: 100%;
  z-index: 1;
}
.img-front {
  grid-column: 1 / 3;
  grid-row: 1;
  width: 80%;
  margin-top: 40px;
  margin-left: 30%;
  z-index: 2;  /* Grid子要素なのでposition不要 */
  box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}

DevTools でのデバッグ方法

z-indexの問題をデバッグする際に、Chrome DevToolsの機能を活用すると効率的に原因を特定できます。

Chrome DevTools の 3D View(Layers パネル)

Chrome DevToolsには、ページの3D表示で要素のレイヤー構造を視覚的に確認できる機能があります。

3D Viewの使い方

  • DevToolsを開く(F12 または Ctrl+Shift+I)
  • 右上の「⋮」メニュー → More tools → Layers を選択
  • または Command Palette(Ctrl+Shift+P)で「Show Layers」と入力
  • 3Dビューでドラッグして回転させ、レイヤーの重なりを確認
  • 各レイヤーをクリックすると、そのレイヤーの詳細情報が表示される

Computed タブで z-index の計算値を確認

要素に適用されているz-indexの実際の値を確認する方法です。

Computedタブでの確認手順

  • 対象の要素を右クリック →「検証」を選択
  • Elements パネルで要素が選択された状態で、右側の「Computed」タブを選択
  • フィルターに「z-index」と入力
  • z-indexの計算値が表示される(autoの場合はautoと表示)
  • 同様に「position」もチェックして、staticになっていないか確認

スタッキングコンテキストの特定方法

ある要素がどのスタッキングコンテキストに属しているかを特定する方法です。

スタッキングコンテキストの特定手順

  • Step 1: 問題の要素をDevToolsで選択
  • Step 2: DOMツリーを上に辿りながら、各祖先要素のComputedスタイルを確認
  • Step 3: 以下のプロパティが設定されている祖先を探す
    • z-indexがauto以外(かつpositionがstatic以外)
    • opacityが1未満
    • transformがnone以外
    • filterがnone以外
    • isolation: isolate
    • position: fixed または sticky
  • Step 4: 最も近い上記の祖先が、その要素が属するスタッキングコンテキスト
JavaScriptでスタッキングコンテキストを検出
// DevTools Consoleで実行するヘルパー関数
function findStackingContext(element) {
  let el = element;
  while (el && el !== document.documentElement) {
    const style = window.getComputedStyle(el);
    const isContext =
      style.position === 'fixed' ||
      style.position === 'sticky' ||
      (style.zIndex !== 'auto' &&
       style.position !== 'static') ||
      parseFloat(style.opacity) < 1 ||
      style.transform !== 'none' ||
      style.filter !== 'none' ||
      style.isolation === 'isolate';

    if (isContext) {
      console.log('スタッキングコンテキスト:', el);
      console.log('  position:', style.position);
      console.log('  z-index:', style.zIndex);
      console.log('  opacity:', style.opacity);
      console.log('  transform:', style.transform);
      return el;
    }
    el = el.parentElement;
  }
  console.log('ルートスタッキングコンテキスト(html)');
  return document.documentElement;
}

// 使い方: 要素を選択してConsoleで実行
findStackingContext($0);  // $0は選択中の要素

ポイント:上記のJavaScript関数をDevToolsのConsoleに貼り付けて使うと、選択した要素が属するスタッキングコンテキストを簡単に特定できます。z-indexのデバッグ時に非常に便利です。

まとめ

z-indexが効かない原因は、大きく分けて7つあります。最後に、それぞれの原因と対処法をチェックリストとしてまとめます。

z-indexが効かない原因チェックリスト

原因 確認ポイント 解決方法
① positionが未指定positionがstaticになっていないかposition: relativeを追加
② スタッキングコンテキスト親要素がコンテキストを作っていないか親のz-indexを調整 or autoにする
③ opacity/transform/filter祖先にこれらのプロパティがないかプロパティを削除 or 代替手段を使う
④ Flexbox/Gridの子要素Flex/Grid子のz-indexがコンテキストを作っていないか不要なz-indexを削除
⑤ isolationisolation: isolateがスコープを作っていないか必要に応じて解除 or 活用
⑥ フレームワーク競合BootstrapやTailwindのz-indexと衝突していないかフレームワークの値に合わせる
⑦ position: fixedの特殊挙動祖先にtransformがあってfixedの基準が変わっていないかfixed要素をDOMの上位に移動

スタッキングコンテキストが作られる条件一覧表(再掲)

条件
position: absolute/relative + z-indexがauto以外position: relative; z-index: 0;
position: fixed / stickyposition: fixed;
Flex/Grid子要素 + z-indexがauto以外親がdisplay: flexで子にz-index: 1
opacity < 1opacity: 0.99;
transform != nonetransform: scale(1);
filter / backdrop-filter != nonefilter: blur(0);
isolation: isolateisolation: isolate;
will-change (transform, opacity等)will-change: transform;
contain: layout / paintcontain: paint;
mix-blend-mode != normalmix-blend-mode: multiply;
perspective != noneperspective: 1000px;
clip-path / mask != noneclip-path: circle(50%);

z-index設計テンプレート

最後に、プロジェクトですぐ使えるz-index設計テンプレートを掲載します。このCSS変数をプロジェクトの:rootに追加して活用してください。

z-index設計テンプレート(コピーして使えます)
/* ============================================
   z-index レイヤー設計テンプレート
   プロジェクトの :root に追加して使う
   ============================================ */
:root {
  /* Layer 0: 背景装飾 */
  --z-below: -1;

  /* Layer 1: 通常コンテンツ */
  --z-base: 0;
  --z-raised: 1;

  /* Layer 2: ナビゲーション */
  --z-dropdown: 10;
  --z-sticky: 20;

  /* Layer 3: オーバーレイ */
  --z-overlay: 30;
  --z-modal: 40;

  /* Layer 4: ポップオーバー */
  --z-popover: 50;
  --z-tooltip: 60;

  /* Layer 5: 通知 */
  --z-toast: 70;

  /* Layer MAX: 緊急時のみ */
  --z-max: 100;
}

z-indexの問題は、スタッキングコンテキストの理解が鍵です。この記事で解説した7つの原因と解決方法を覚えておけば、z-indexに悩まされることは格段に減るはずです。

問題が発生したときは、以下の3ステップで対処しましょう。

z-indexトラブルシューティング 3ステップ

  • Step 1: DevToolsでpositionz-indexの計算値を確認する
  • Step 2: 祖先要素をたどり、スタッキングコンテキストを作っている要素を特定する
  • Step 3: コンテキストの原因(opacity, transform, z-index等)を特定し、適切な解決方法を選ぶ