z-indexのカオスを卒業する——マジックナンバーを廃止し、トークンで管理する設計手法

z-indexのカオスを卒業する——マジックナンバーを廃止し、トークンで管理する設計手法

z-indexのカオスを卒業する——マジックナンバーを廃止し、トークンで管理する設計手法

CSSの `z-index` は、要素の重なり順を制御するための強力なプロパティだ。モーダルやトースト、ドロップダウンなど、現代のUI(ユーザーインターフェース)実装において欠かすことはできない。

しかし、プロジェクトが大規模になるにつれ、`z-index` の値は制御不能な「マジックナンバー」の温床となる。場当たり的に指定された巨大な数値がコードベースを侵食し、修正が困難なバグを引き起こす。

本記事では、`z-index` の軍拡競争を終わらせるための「トークン化」による管理手法を解説する。この仕組みを導入することで、重なりの優先順位を論理的に整理し、保守性の高いコードを実現できる。

z-indexが引き起こす「軍拡競争」の実態

z-indexが引き起こす「軍拡競争」の実態

多くの開発現場で、`z-index: 10001` のような不自然に大きな数値を目にすることがある。なぜこのような「マジックナンバー」が生まれるのか。その背景には、開発者が抱く「要素が隠れてしまうことへの恐怖」がある。

なぜ「10001」のような数字が生まれるのか

複数のチームが並行して開発を行う大規模プロジェクトでは、画面上に何が浮いているかを完全に把握するのは難しい。Aチームが作った通知、Bチームのクッキーバナー、マーケティング用のSDKが生成するモーダルなどが混在する。

開発者は「とにかく一番上に表示させたい」という一心で、既存のどの要素よりも大きいと思われる数値を勘で入力する。これが「マジックナンバー」の正体だ。マジックナンバーとは、文脈や根拠がなく、その場しのぎで設定された特定の数値を指す。

一度この軍拡競争が始まると、次の開発者はさらに大きな数値を設定せざるを得なくなる。最終的に `9999999` のような極端な値が並び、コードの意図は完全に消失する。

ブラウザが許容する最大値の罠

`z-index` には設定可能な最大値が存在する。多くのブラウザでは **2147483647** が上限だ。これは32ビット符号付き整数の最大値に由来する。

この数値を超えて指定しても、ブラウザによってこの上限値に丸められる。つまり、無限に数値を大きくして「勝ち続ける」ことは不可能だ。数値の大きさで解決しようとするアプローチは、いずれ技術的な限界に突き当たる。

重ね合わせ文脈(Stacking Context)の基本

重ね合わせ文脈(Stacking Context)の基本

`z-index` の問題を難しくしているのは、数値の大小だけで重なりが決まらない点にある。ここで重要になるのが「重ね合わせ文脈(Stacking Context)」という概念だ。

値の大きさよりも「親」が優先される仕組み

重ね合わせ文脈とは、要素の重なりを計算するための独立したグループのようなものだ。例えるなら、書類の束(スタック)が入った「フォルダ」をイメージすると分かりやすい。

どれほど大きな `z-index` を持っていたとしても、その要素が属する「フォルダ(親の重ね合わせ文脈)」自体が低い位置にあれば、他のフォルダより前に出ることはできない。

以下のコードで、その挙動を確認できる。

/* 親要素が重ね合わせ文脈を作る */
.parent-low {
  position: relative;
  z-index: 1;
}

.parent-high {
  position: relative;
  z-index: 2;
}

/* 子要素に大きな値を指定しても、親の z-index: 1 に縛られる */
.child-massive {
  position: absolute;
  z-index: 9999;
}
親1(z-index: 1)

(z: 9999)
親2(z-index: 2)

子要素はz-index:9999だが、親1(z:1)に縛られ、親2(z:2)の下に隠れている

このデモでは、青い子要素に `z-index: 9999` を指定しているが、親要素の `z-index: 1` という制約により、隣にある `z-index: 2` の親要素(緑)の下に潜り込んでしまう。

このように、`z-index` のトラブルの多くは数値の不足ではなく、重ね合わせ文脈の構造に起因している。

CSS変数(トークン)による設計の体系化

CSS変数(トークン)による設計の体系化

マジックナンバーを排除し、プロジェクト全体で一貫した重なり順を維持するための最も有効な手段は、CSS変数(カスタムプロパティ)を用いた「トークン化」だ。

グローバルトークンで「階層」を定義する

まず、アプリケーション全体で共有する「レイヤー」を定義する。具体的な数値ではなく、その要素が果たす役割(役割ベース)で命名するのがポイントだ。

:root {
  --z-base: 0;
  --z-sticky: 100;
  --z-dropdown: 200;
  --z-overlay: 300;
  --z-modal: 400;
  --z-popover: 500;
  --z-toast: 600;
}

このように定義しておけば、開発者は「モーダルだから `–z-modal` を使おう」と判断するだけで済む。数値の管理は `:root` の一箇所に集約されるため、後から「トーストをモーダルの背面に移動したい」といった変更が必要になっても、変数の値を入れ替えるだけで全要素に反映される。

calc() を使った相対的なレイヤリング

特定の要素に対して、基準となるレイヤーから少しだけ浮かせたい、あるいは沈ませたい場合がある。例えば、モーダルの背面に敷く背景(バックドロップ)などだ。

この場合、新しいトークンを作るのではなく `calc()` を利用して相対的に指定する。

.modal-backdrop {
  /* モーダルのトークンより常に 1 だけ背面に配置 */
  z-index: calc(var(--z-modal) - 1);
}

これにより、要素間の主従関係がコード上で明示される。`–z-modal` の値が変更されても、バックドロップは常にその背後を追従するため、関係性が崩れる心配がない。

コンポーネント内部での「ローカル管理」

コンポーネント内部での「ローカル管理」

グローバルなトークンは便利だが、あらゆる要素をグローバル変数で管理しようとすると、変数の数が膨大になり管理が破綻する。そこで、コンポーネント内部で完結する「ローカル管理」を併用する。

–z-top と –z-bottom の導入

コンポーネントが独自の重ね合わせ文脈(Stacking Context)を持っている場合、その内部での重なり順はグローバルな値とは無関係になる。

例えば、モーダル内の閉じるボタンと背景装飾の重なりを制御する場合、グローバルトークンを使う必要はない。以下のように、コンポーネント固有の「基準値」を定義するのが賢明だ。

.my-component {
  /* 重ね合わせ文脈を強制的に作成 */
  isolation: isolate;
  z-index: var(--z-overlay);
}

.my-component__decoration {
  /* コンポーネント内の底辺 */
  z-index: -1;
}

.my-component__close-button {
  /* コンポーネント内の最前面 */
  z-index: 10;
}

`isolation: isolate` は、その要素に新しい重ね合わせ文脈を強制的に作成するプロパティだ。これを使うことで、内部の `z-index` が外部に影響を与えたり、外部の影響を受けたりすることを防ぐ「安全地帯」を作ることができる。

ツールチップやモーダル内での活用

ツールチップのように「どこにでも現れる」コンポーネントは、管理が最も難しい。しかし、これもローカルな視点で考えればシンプルになる。

ツールチップは、常に「自分を呼んだ要素」のすぐ上にいればよい。そのため、コンポーネント内で `z-index: 1` 程度の小さな値を設定するだけで十分だ。そのツールチップがモーダル内で使われれば、モーダルの重ね合わせ文脈の中で最前面に立ち、メインコンテンツで使われればそこで最前面に立つ。

システムを維持するための自動化とルール

システムを維持するための自動化とルール

優れた設計も、運用が徹底されなければ形骸化する。特に納期が迫った状況では、つい `z-index: 999` と書き込みたくなるのが開発者の性だ。これを防ぐには、仕組みによる強制が必要だ。

Linterによるマジックナンバーの禁止

Stylelintなどの静的解析ツールを導入し、`z-index` プロパティに直接数値を記述することを禁止する。

例えば、`stylelint-declaration-strict-value` というプラグインを使えば、`z-index` には変数(`var()`)しか使えないように制限できる。

/* .stylelintrc.json */
{
  "plugins": ["stylelint-declaration-strict-value"],
  "rules": {
    "scale-unlimited/declaration-strict-value": ["z-index"]
  }
}

ビルドプロセスでエラーが出るようになれば、開発者は必然的に定義されたトークンを確認し、適切なレイヤーを選択するようになる。

z-index 設計の黄金律

最後に、保守性の高い `z-index` 管理を維持するためのルールをまとめる。

  • マジックナンバーを使わない: 根拠のない数値はバグの元だ。
  • トークンを必須とする: すべての値は設計された変数から取得する。
  • 重なりがおかしい時は構造を疑う: 数値を増やす前に、重ね合わせ文脈(親要素の z-index や opacity)を確認する。
  • 意味のある単位で刻む: 1, 2, 3 ではなく 100, 200, 300 と刻むことで、後からの割り込み(150など)に対応しやすくなる。
  • calc() で関係を縛る: 背景と本体のようにセットで動くものは、計算式で結合する。

`z-index` の価値は、数値の大きさではなく、それが属する「システム」の整合性にある。カオスな現状を打破し、予測可能なUI実装を目指すべきだ。

この記事のポイント

  • z-indexの軍拡競争は、数値ではなく「役割ベースのトークン」で解決する。
  • 重ね合わせ文脈(Stacking Context)を理解し、親要素の影響を考慮する。
  • グローバルトークンと、コンポーネント内のローカル管理を使い分ける。
  • Stylelintなどのツールを用いて、マジックナンバーの混入を自動的に防ぐ。
  • calc() を活用して、要素間の相対的な重なり関係をコードに明文化する。

出典

  • CSS-Tricks「The Value of z-index」(2026年3月9日)
  • MDN Web Docs「The stacking context」(2025年12月15日)
海田 洋祐

・ 複数業界における17年間のデジタルビジネス開発経験 ・ ウェブサイト開発のためのHTML、PHP、CSS、Java等の実用的知識 ・ 15ヶ国語対応の多言語SaaSの開発経験 ・ 17年間にも及ぶ、Eコマース長期運営経験 ・ 幅広い業界でのSEO最適化の豊富な経験

メッセージを残す