CSSz-indexCSS Variables前端工程程式碼品質

z-index 管理:告別魔法數字的軍備競賽

告別 z-index: 9999 的混亂時代!本文介紹如何透過 CSS 自訂屬性(Token)系統性管理層級,讓你的程式碼更可控、可維護。

· 4 分鐘閱讀

如果你曾在 CSS 裡寫過 z-index: 9999,恭喜你,你已經參與了這場「層級軍備競賽」。但有沒有想過,為什麼我們需要用這麼大的數字?答案是:我們從一開始就錯了。


魔法數字的起源

打開任何一個有點歷史的專案,你很可能會看到這樣的程式碼:

.modal { z-index: 1000; }
.dropdown { z-index: 500; }
.toast { z-index: 9999; }
.sticky-header { z-index: 100; }
.tooltip { z-index: 2000; }

這些數字是怎麼來的?通常是開發者「試試看」之後發現「啊,這個不夠高,再調高一點」。然後下一個人遇到同樣的問題,於是寫下了 z-index: 10000。這就是所謂的「魔法數字」——沒有人知道為什麼是這個數字,但它就這樣存在著。

問題來了:

  1. 無法預測:新增一個元件時,你不知道該用什麼數字
  2. 衝突風險:不同元件可能使用相同的 z-index,導致層級錯亂
  3. 維護困難:要調整層級關係,必須一個一個改
  4. 無意義的數字999910000 有什麼本質區別?沒有

這就是所謂的「軍備競賽」——誰寫的數字大,誰就贏了。但這種比賽沒有贏家,只有技術債。


Token 化:系統性的解決方案

解決方案很簡單:不要用隨機數字,用命名來代表層級。這就是所謂的「Token 化」——將 z-index 值抽象成有意義的名稱。

建立你的 z-index 系統

首先,在你的 CSS 根層級定義一套 z-index token:

:root {
  /* 基礎層級 - 最底層 */
  --z-base: 0;
  
  /* 略高於內容 */
  --z-above-content: 10;
  
  /* 懸停效果、active 狀態 */
  --z-hover: 50;
  
  /* 固定導航列 */
  --z-sticky: 100;
  
  /* 下拉選單、浮動按鈕 */
  --z-dropdown: 200;
  
  /* 提示工具 */
  --z-tooltip: 300;
  
  /* 彈出式訊息(Toast) */
  --z-toast: 400;
  
  /* 對話框、Modal */
  --z-modal: 500;
  
  /* 確認視窗(Confirm Dialog) */
  --z-confirm: 600;
  
  /* 最頂層:載入中、全螢幕遮罩 */
  --z-overlay: 1000;
}

這套系統的重點是什麼?數字之間有足夠的間距。這樣你可以在中間插入新的層級,而不需要重新調整整個系統。

使用方式

現在,當你需要設定 z-index 時,直接使用這些 token:

.navbar {
  position: sticky;
  z-index: var(--z-sticky);
}

.dropdown-menu {
  position: absolute;
  z-index: var(--z-dropdown);
}

.modal {
  position: fixed;
  z-index: var(--z-modal);
}

.loading-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
}

這樣一來,層級關係一目了然。你不需要記住哪個元件應該用什麼數字——只要知道它的「語意」就可以了。


calc() 的妙用:相對層級

但有時候,你可能需要一些「動態」的層級。比如說,一個 modal 裡面的 tooltip,應該比 modal 本身高,但比最頂層低。這時候 calc() 就派上用場了。

相對層級語法

.modal-tooltip {
  /* modal 的層級 + 1 */
  z-index: calc(var(--z-modal) + 1);
}

.modal-close-button {
  /* modal 內部的關閉按鈕,稍微高一點 */
  z-index: calc(var(--z-modal) + 1);
}

.confirm-dialog {
  /* 比普通 modal 高,但比 overlay 低 */
  z-index: calc(var(--z-overlay) - 100);
}

這種方式的優點是什麼?你可以建立「相對於某個基礎值」的層級,而不需要記住絕對數字。

進階:動態層級

如果你使用 JavaScript 動態建立元件(例如 React 的 Portal),你可能需要動態計算 z-index:

// 假設我們需要一個 modal,其 z-index 基於當前頁面的 modal 數量
let modalCount = 0;

function openModal() {
  const zIndex = 500 + modalCount; // 從 500 開始遞增
  modalCount++;
  
  return {
    zIndex,
    close: () => {
      modalCount--;
    }
  };
}

但在大多數情況下,靜態的 token 系統就足夠了。動態計算通常是不必要的複雜。


實務建議:建立團隊共識

Token 化 z-index 不只是技術問題,更是團隊協作的問題。以下是一些實務建議:

1. 文件化你的層級系統

在你的專案文件或 CSS 開頭註解中,說明每個層級的用途:

/*
 * z-index 層級系統
 * 
 * 0-99:   基礎內容層(預設)
 * 100-199: 導航與固定元素
 * 200-299: 浮動與互動元素
 * 300-399: 提示與小型彈出
 * 400-499: 重要通知
 * 500-599: 主要對話框
 * 600-699: 高優先權對話框
 * 1000+:  全螢幕覆蓋層
 */

2. 避免「層級蔓延」

有些團隊會忍不住一直增加新的層級:「這個要在 modal 上面」「那個要在 confirm 上面」。時間久了,你的 z-index 系統會變得跟原本的魔法數字一樣混亂。

解決方法是:嚴格限制層級數量。只有當真的有需要跨越多個層級的元件時,才考慮新增。

3. 考慮使用 CSS @layer

CSS 的 @layer 為你提供了另一個管理層級的工具:

@layer base {
  .content { z-index: 1; }
}

@layer components {
  .card { z-index: 10; }
}

@layer overlays {
  .modal { z-index: 100; }
}

@layer 的好處是:即使你在不同 layer 中定義了相同的 z-index,後面的 layer 會自動覆蓋前面的。但這是一個更進階的主題,需要另外詳細介紹。


總結

告別 z-index: 9999 的時代吧!透過系統性的 Token 管理,你可以:

  • 提升可讀性:層級關係一目了然
  • 降低維護成本:新增、調整元件不再需要東改西改
  • 減少衝突:明確的命名避免意外的層級覆蓋
  • 團隊協作:統一的命名讓程式碼更容易理解

z-index 不應該是一場軍備競賽。它應該是一個有紀律的系統。

從今天開始,讓你的 CSS 變數成為你的層級管理幫手。


你有自己管理 z-index 的方法嗎?歡迎在 comments 分享你的經驗!