cover

結論先講

你寫 .button { color: red; } 但樣式沒生效。Debug 後發現是別處的 #app .button { color: blue; } 贏了。這不是 bug,是 Specificity(優先級) 在運作。

搞懂 Specificity 你會:

  • 不再亂加 !important(大家都加就大家都輸)
  • 知道為什麼第三方 CSS 會蓋掉你的
  • 會用 @layer(現代 CSS 的乾淨解法)

CSS 決定「誰贏」的順序:來源 → Cascade Layer → Specificity → 出現順序。這篇逐層拆解。


Cascade:四層判斷順序

CSS 有多個來源的規則同時生效時,瀏覽器用這個順序決定哪個贏:

  1. 來源(Origin)+ 重要性(Importance)
  2. Cascade Layer(@layer)
  3. Specificity(優先級)
  4. 出現順序(source order)

逐層往下比,上層贏就決定,不會看下層。

Step 1:來源 + 重要性

順序來源例子
1(輸)瀏覽器預設user-agent stylesheet
2使用者樣式瀏覽器 extension 的 CSS
3作者樣式你寫的 CSS
4作者 !important.foo { color: red !important; }
5使用者 !important無障礙覆寫
6(贏)瀏覽器 !important極少見

你寫的 CSS 通常是 3。加 !important 升到 4。


Specificity(優先級計算)

同一來源的多個規則,看 specificity(特異性)分數。

計分方式(三欄:A B C)

a, b, c 三欄記分(不是十進位):

選擇器類型加分位置例子
IDA#header → (1,0,0)
Class、屬性、偽類B.btn[type=text]:hover → (0,1,0)
元素、偽元素Cdiv::before → (0,0,1)

比較規則

從左到右比較:

  • (0,1,0) > (0,0,5)(B 贏過 C,即使 C 多)
  • (1,0,0) > (0,99,99)(A 永遠贏 B)

實例計分

/* 選擇器                  分數         */
div                        /* (0,0,1) */
.btn                       /* (0,1,0) */
.btn:hover                 /* (0,2,0) */
.card .btn                 /* (0,2,0) */
.card > .btn               /* (0,2,0)  -- 組合子不加分 */
#app .btn                  /* (1,1,0) */
#app .card .btn            /* (1,2,0) */
div.btn                    /* (0,1,1) */
[data-theme=dark] .btn     /* (0,2,0) */
.btn.primary.large         /* (0,3,0) */

特殊情況

  • * 不加分:(0,0,0)
  • :is() / :has() / :not() 本身不加分,但取括號內最高的加分
    • :is(.a, #b) 算 (1,0,0)
    • :not(.a) 算 (0,1,0)
  • :where() 永遠 (0,0,0) — 這是現代 CSS 的「低優先級」工具

!important:為什麼不要亂用

!important 把規則從「作者層」升到「作者 !important 層」(Cascade Step 1 的第 4 格)。

壞處

  • 別人要蓋掉你的 !important,只能也用 !important → 開始軍備競賽
  • 三個月後你自己要改也蓋不掉,最後改成 !important !important(開玩笑,但大家的心態)
  • 整個專案的 CSS 變成誰的 !important 多誰贏

合理使用時機

  • Utility CSS(Tailwind 的 ! prefix)— 設計就是要覆寫
  • Overriding 第三方 CSS 且無法改源碼
  • Debug 臨時測試

一般寫 CSS 不該用

永遠用「提高 specificity」或「改用 @layer」來代替 !important


@layer:現代 CSS 的救星

Cascade Layers 讓你明確控制哪層 CSS 贏,不靠 specificity。

@layer base, components, utilities;
 
@layer base {
  button { background: gray; }  /* specificity 0,0,1 */
}
 
@layer components {
  .button { background: blue; }  /* specificity 0,1,0 */
}
 
@layer utilities {
  .bg-red { background: red; }   /* specificity 0,1,0 */
}

即使 specificity 相同後宣告的 layer 贏(utilities 贏 components 贏 base)。

沒進 layer 的規則呢?

@layer base { .btn { color: blue; } }
 
.btn { color: red; }  /* 沒在 layer 裡 */

沒進 layer 的規則永遠贏過 layered 規則(即使 layered 宣告在後)。用這個特性放「絕對要贏」的規則。

實戰:為什麼要用

傳統 CSS 的痛點:

/* 載入順序:reset → components → my-override */
 
/* reset.css */
button { padding: 0; }
 
/* components.css */
.primary-btn { padding: 10px; }  /* specificity (0,1,0) 贏 reset */
 
/* my-override.css */
button.primary-btn { padding: 20px !important; }  /* 只能這樣蓋 */

@layer 後:

@layer reset, components, overrides;
 
@layer reset { button { padding: 0; } }
@layer components { .primary-btn { padding: 10px; } }
@layer overrides { button { padding: 20px; } }  /* 自動贏,不用 !important */

乾淨又可預測。現代瀏覽器都支援(Chrome 99+、Firefox 97+、Safari 15.4+)。


出現順序(source order)

前兩層都平手時,後面的贏

.btn { color: red; }
.btn { color: blue; }  /* 贏:出現在後面 */

這就是為什麼 reset.css 要最先載入、overrides.css 最後載入。


Debug Specificity 的工具

Chrome DevTools 的 Computed 面板

選元素 → Styles 面板:

  • 劃掉的規則表示被其他規則蓋掉
  • 右邊顯示每條規則來自哪個檔案跟行數
  • 按 F12 → Computed 看最終值

VS Code 的 Specificity 插件

CSS Specificity 擴充套件直接在選擇器旁顯示分數,寫 CSS 時即時提醒。

線上計算器

specificity.keegan.st — 貼選擇器算分。


實戰策略

策略 1:降 specificity,用 class

❌ 避免:

div#app .card ul li a { color: blue; }  /* (1,2,3) 很難蓋 */

✅ 偏好:

.card-link { color: blue; }  /* (0,1,0) 容易管理 */

策略 2:用 :where()

/* 0 specificity,永遠好蓋掉 */
:where(h1, h2, h3, h4) { font-weight: 600; }

策略 3:@layer 管組織

@layer reset, theme, components, utilities;

用 layer 管優先,不靠 specificity。

策略 4:選擇器命名盡量單一

BEM 命名法:.card__title--large 避免巢狀選擇器。


實戰 Checklist

  • 避免 ID 選擇器(太高 specificity)
  • 避免深度巢狀(.a .b .c .d
  • 不要隨便用 !important
  • 現代專案用 @layer 組織
  • 重設 CSS 用 :where() 保持 0 specificity
  • Debug 時看 DevTools 的 Computed + Styles

相關文章