🫠

もう z-[9999] は書かせない、z-index とフロントエンドレイヤー設計

に公開

どんな話?

こんにちは、シロクでフロントエンド開発をしている小野寺です。
今回は z-index の運用設計と戦略を整理して、チーム全体で破綻しない仕組みにした話です。
Tailwind / Next.js 環境で z-index の運用ルールをどう設計・運用しているか を紹介します。
プロジェクトが進むにつれて「z-index の数字競争」が起こりがちですが、
本記事ではそれを防ぐための設計・Lint チェック・チーム運用までをまとめました。

背景

実際にシロクのプロダクトで運用されている「z-[9999999]」を使った実装のスクリーンショット

これは、実際にシロクのプロダクトで運用されている z-index です。
見ての通り、z-[9999999] というびっくりする数値が使われています。

一見、「動いている」ように見えても、
新しい機能を追加するときに「どのレイヤーを上にすればいいのか」が誰にもわからない。
修正のたびに「とりあえず 1 足してみる」文化が生まれ、
結果として z-index の数字が無限に増殖していく——そんな課題を抱えていました。

同時に、社内では Next.js ベースのフロントエンドリプレイスを進めています。
このリプレイスでは Tailwind CSS を採用しており、スタイリング基盤を一新するタイミングでもありました。

せっかくリプレイスを進めるなら、
この「z-index の無法地帯」をきちんと整理しておきたい。
そんな思いから、z-index の運用と戦略をチームで再設計する取り組みを始めました。

まず前提:なぜ破綻するのか

z-index が破綻するのは、単純に言えば 「全体の基準がない」 ことに尽きます。
それぞれが「必要な場面でちょっと上に出したい」という判断を積み重ねていくと、
スタッキングコンテキストの理解が曖昧なまま数値が膨らみ、いつしか誰も全体を把握できなくなります。

結果、「なぜモーダルがヘッダーの下に潜ってる?」のような事故が起きます。

レイヤー設計の考え方

参考にしたのは、OutSystems UI Layer System という記事です。
ここで紹介されていた Layer System の考え方をベースに、
シロクのデザインシステムに合わせた階層構造を定義しました。

5つのレイヤーで整理する

プロダクト全体を次の5階層に整理しました。
「どの要素がどの層に属するか」を明確にしておくことで、
誰でも一目で判断できるようにしています。

レイヤー名 z-index値 CSS変数 用途 使用例
z-background -1 --z-background 背景要素、パターン ページ背景など
z-content 0 --z-content 通常のコンテンツ ボタン、テキストなど
z-nav 500 --z-nav ナビゲーション、固定ヘッダー スティッキーナビなど
z-overlay 800 --z-overlay ドロップダウン、ツールチップ ポップアップなど
z-system 1000 --z-system モーダル、アラート、トースト ダイアログなど

Tailwind での定義方針

基本方針は以下の2つです。

  1. 固定ユーティリティクラスとして定義する
  2. 自由記述は原則禁止(ただし明示的な例外は許容)

Tailwind の設定は次のようにしています。

Tailwind v3 の場合

// tailwind.config.ts
zIndex: {
  background: '-1',
  content: '0',
  nav: '500',
  overlay: '800',
  system: '1000',
},
plugins: [
  ({ addBase, theme }) => {
    addBase({
      ':root': {
        '--z-background': theme('zIndex.background'),
        '--z-content': theme('zIndex.content'),
        '--z-nav': theme('zIndex.nav'),
        '--z-overlay': theme('zIndex.overlay'),
        '--z-system': theme('zIndex.system'),
      },
    });
  },
],

Tailwind v4 の場合

/* globals.css */
@import "tailwindcss";

@theme {
  --z-background: -1;
  --z-content: 0;
  --z-nav: 500;
  --z-overlay: 800;
  --z-system: 1000;
}

この設定により、Tailwind 側で .z-system.z-overlay のようなクラスを直接利用できます。

<div className="z-system" />

チームとしては「arbitrary value(例:z-[9999])」を使わない方針を取っています。
あらかじめ定義したユーティリティクラスのみを利用することで、自由度を制限して秩序を保つスタイルです。

例外は calc() で ±1 のみ許容

ただし、完全に自由を封じると逆に困るケースもあります。
例えば、モーダル内でさらに一段上の要素を表示したいとき。

そのため、calc() を使って「レイヤー ±1」を許容しています。

<div className="z-[calc(var(--z-system)+1)]" />

このように書けば、z-system より1段上のコンテキストを作れます。
ただし、数字の直書きは禁止です。
これも ESLint ルールで静的に制御しています。

ESLint でルールを縛る

ルールを作っても、守られなければ意味がありません。
そこで、ESLint で z-index の使い方を縛る仕組みを入れました。

基本的には z-background, z-content, z-nav, z-overlay, z-system のような
定義済みユーティリティクラスだけを許可
ただし、運用上どうしても必要になる一部の例外(calc() による ±1)だけは許容しています。

const ALLOWED_Z_INDEX_CLASSES = ['z-background', 'z-content', 'z-nav', 'z-overlay', 'z-system'];

const ALLOWED_CSS_VAR_PATTERN =
  /^z-\[(var\(\s*--z-[a-z-]+\s*\)|calc\(\s*var\(\s*--z-[a-z-]+\s*\)\s*[-+]\s*1\s*\))\]$/;

このルールで、

  • z-[9999] → ❌
  • z-system → ✅
  • z-[calc(var(--z-system)+1)] → ✅

という感じで弾きます。

現在は 正規表現ベースで className の文字列を検査する実装ですが、
将来的には AST(抽象構文木)での解析に移行する予定です。

Tailwind の場合はテンプレートリテラルでクラス名を組み立てても基本的に静的解析が効くため、
現状でも正規表現で十分機能しています。
ただし、将来的に clsx() などを用いた動的クラス指定が増えた際に、
AST レベルでより厳密に「どのノードで不正な z-index が指定されたか」を検知できるようにする予定です。

CI にも組み込んでおり、レビュー時に特別な意識をしなくても、
ルールが自動で縛ってくれる状態になっています。

運用してみて

実はこのルールを導入してみて間もないのですが、実装する上で「どの要素をどのレイヤーに置くか」の判断がつきやすくなりました。
運用方針のドキュメントを読ませることで、AI コーディングでも正しい z-index の運用が可能となっています。

まとめ

  • z-index は「数字」ではなく「責務」で管理
  • Tailwind に固定ユーティリティとして定義して自由を制限
  • ただし calc による ±1 だけを例外的に許可
  • ESLint + CI でルールを縛り、破綻を防止
  • 現在は正規表現チェック、今後は AST 化によるチェック厳密化を予定

数字の競争を終わらせるには、まずルールをコード化することが大事です。
もしよければ参考にしていただけると幸いです。

シロク エンジニアブログ

Discussion