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でのデバッグ方法
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: autoとz-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要素のスタイル |
| 2 | z-indexが負の値のpositioned要素 | z-index: -1 など |
| 3 | 通常フローのブロック要素 | div, p, section など |
| 4 | フロート要素 | float: left / right |
| 5 | 通常フローのインライン要素 | span, a, テキスト |
| 6 | z-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以外(relative、absolute、fixed、sticky)に設定された要素にのみ適用されます。
問題のコード
以下のコードでは、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を追加しても、top、right、bottom、leftを指定しない限り要素の位置は変わりません。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の値 |
主な用途 |
特徴 |
relative | z-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: autoとz-index: 0の違い。見た目の重ね順は同じですが、z-index: 0は新しいスタッキングコンテキストを作成し、z-index: autoは作成しません。これがz-index問題の核心です。
新しいスタッキングコンテキストが作られる条件一覧
以下の条件のいずれかを満たす要素は、新しいスタッキングコンテキストを作成します。
| 条件 |
例 |
頻度 |
| ルート要素(html) | 常にルートコンテキスト | – |
| position: absolute/relative + z-indexがauto以外 | position: relative; z-index: 1; | ★★★ |
| position: fixed / sticky | position: 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: isolate | isolation: isolate; | ★★ |
| will-changeで上記プロパティを指定 | will-change: transform; | ★★ |
| contain: layout / paint / strict / content | contain: paint; | ★ |
★が多いほど実際の開発でよく遭遇します。特にposition + z-index、opacity、transformは最も高頻度で問題の原因となります。
異なるスタッキングコンテキスト間の重ね順
異なるスタッキングコンテキストに属する要素同士の重ね順は、以下の手順で決まります。
重ね順の決定アルゴリズム
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)より上に表示されてしまう! */
}
なぜ効かないのか:.cardにopacity: 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 でも作られる
filterやbackdrop-filterもnone以外で新しいスタッキングコンテキストを作成します。
❌ 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-changeでopacity、transform、filter等を指定すると、実際にそのプロパティが変更されていなくても新しいスタッキングコンテキストが作成されます。
will-changeの注意点
/* ❌ 常時指定するとコンテキストが作られっぱなし */
.container {
will-change: transform;
/* transform自体は指定していなくてもコンテキストが作られる */
}
/* ✅ 必要な時だけ適用 */
.container {
will-change: auto;
}
.container:hover {
will-change: transform;
}
contain: paint / layout の影響
containプロパティのpaintやlayoutも新しいスタッキングコンテキストを作成します。
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: paint → contain: 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変数 |
| Dropdown | 1000 | $zindex-dropdown |
| Sticky | 1020 | $zindex-sticky |
| Fixed | 1030 | $zindex-fixed |
| Offcanvas backdrop | 1040 | $zindex-offcanvas-backdrop |
| Offcanvas | 1045 | $zindex-offcanvas |
| Modal backdrop | 1050 | $zindex-modal-backdrop |
| Modal | 1055 | $zindex-modal |
| Popover | 1070 | $zindex-popover |
| Tooltip | 1080 | $zindex-tooltip |
| Toast | 1090 | $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の基準が変わる
これは特に厄介な挙動です。親要素にtransform(none以外)が指定されていると、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を使う場合は、祖先要素にtransform、filter、perspectiveが設定されていないか確認しましょう。これらが設定されていると、fixedの挙動が完全に変わります。
fixedの基準が変わるプロパティ一覧
以下のプロパティが祖先要素に設定されていると、position: fixedの基準がビューポートからその祖先要素に変わります。
| プロパティ |
fixedの基準が変わる条件 |
transform | none以外のすべての値 |
perspective | none以外の値 |
filter | none以外の値 |
backdrop-filter | none以外の値 |
will-change | transform, perspective, filterを指定 |
contain | paint を含む値 |
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 | 背景装飾 | 背景パターン、装飾的画像 |
| Content | 0 – 1 | 通常コンテンツ | テキスト、画像、カード |
| Navigation | 10 – 20 | ナビゲーション | ドロップダウン、スティッキーヘッダー |
| Overlay | 30 | オーバーレイ背景 | モーダル背景、ドロワー背景 |
| Modal | 40 | モーダル/ダイアログ | モーダル、確認ダイアログ |
| Popover | 50 – 60 | ポップオーバー | ツールチップ、ポップオーバー |
| Notification | 70 | 通知 | トースト通知、アラート |
| Emergency | 100 | 緊急時のみ | エラーオーバーレイ |
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と競合しないか?
- ☐
opacity、transform、filterがスタッキングコンテキストを作っていないか?
- ☐ モバイル表示でも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を削除 |
| ⑤ isolation | isolation: 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 / sticky | position: fixed; |
| Flex/Grid子要素 + z-indexがauto以外 | 親がdisplay: flexで子にz-index: 1 |
| opacity < 1 | opacity: 0.99; |
| transform != none | transform: scale(1); |
| filter / backdrop-filter != none | filter: blur(0); |
| isolation: isolate | isolation: isolate; |
| will-change (transform, opacity等) | will-change: transform; |
| contain: layout / paint | contain: paint; |
| mix-blend-mode != normal | mix-blend-mode: multiply; |
| perspective != none | perspective: 1000px; |
| clip-path / mask != none | clip-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で
positionとz-indexの計算値を確認する
- Step 2: 祖先要素をたどり、スタッキングコンテキストを作っている要素を特定する
- Step 3: コンテキストの原因(opacity, transform, z-index等)を特定し、適切な解決方法を選ぶ