CSS Cascade, Layers & Specificity — The Full Resolution Order in 2026
How browsers resolve CSS in 2026: origin, @layer, (a,b,c) specificity, :is/:where/:has(), and architecture patterns that end specificity wars.
Tại sao Cascade vẫn quan trọng
Mỗi stylesheet bạn ship là một cuộc thi. Hàng chục — đôi khi hàng nghìn — rule cùng nhắm vào một phần tử. Trình duyệt không đoán; thuật toán cascade xác định để chọn đúng một declaration thắng cho mỗi property.
Nếu bạn chỉ nhớ “ID thắng class”, bạn sẽ thua những trận mà layer, importance, và thứ tự nguồn quyết định. Kỹ sư senior cần toàn bộ stack — không phải cheat sheet từ 2012.
Thứ tự giải quyết Cascade (2025–2026)
Khi hai declaration xung đột trên cùng element và property, trình duyệt so sánh theo thứ tự này. Khác biệt đầu tiên thắng; các bước sau không bao giờ chạy:
| Step | Factor | Ý nghĩa |
|---|---|---|
| 1 | Origin & importance | User agent → user → author; !important đảo author vs user |
| 2 | Cascade layer | @layer Thứ tự @layer; style không layer thắng mọi layer |
| 3 | Specificity | Bộ (a, b, c) |
| 4 | Source order | Rule cuối trong document thắng |
┌─────────────────────────────────────────────────────────────┐
│ 1. Origin + !important │
│ author normal < author !important < inline < ... │
├─────────────────────────────────────────────────────────────┤
│ 2. @layer (normal declarations) │
│ layer A < layer B < unlayered │
├─────────────────────────────────────────────────────────────┤
│ 3. Specificity (a, b, c) — compared lexicographically │
├─────────────────────────────────────────────────────────────┤
│ 4. Source order — later wins │
└─────────────────────────────────────────────────────────────┘
Insight quan trọng: specificity là bước 3, không phải bước 1. Một utility
(0, 1, 0)trong@layersau thắng selector component(1, 2, 0)trong layer trước — không cần!important.
Demo trực tiếp
Dùng calculator để gõ selector bất kỳ và nhận bộ (a, b, c). Visualizer layer cho thấy ba rule với specificity khác nhau cạnh tranh trên một element — bật/tắt thứ tự layer và xem kết quả thay đổi.
Mở demo đầy đủ:
Specificity: Bộ (a, b, c)
Specificity là bộ đếm ba cột, không phải một số nguyên. So a trước, rồi b, rồi c:
| Column | Counts | Examples |
|---|---|---|
| a | ID selectors | #header, #main |
| b | Class, attribute, pseudo-class | .btn, [type="text"], :hover, :focus-visible |
| c | Type, pseudo-element | div, span, ::before, ::placeholder |
Selector phổ quát *, combinator (>, +, ~), và wrapper negation của :where() không cộng gì.
Ví dụ có lời giải
/* (0, 0, 1) — one type */
li { }
/* (0, 1, 1) — one class + one type */
ul.menu li { }
/* (0, 2, 1) — two classes + one type */
.nav .item a { }
/* (1, 1, 1) — one ID + one class + one type */
#sidebar .nav a { }
/* (1, 2, 1) — from the demo */
#nav .item a:hover { }
Với #nav .item a:hover:
#nav → a = 1
.item → b = 1
a → c = 1
:hover → b = 1
─────────────────────
Total (1, 2, 1)
Inline style và !important
Không thay đổi bộ tuple; chúng thay đổi bước cascade nào áp dụng:
| Mechanism | Hiệu ứng cascade |
|---|---|
style="color: red" | Origin inline author — thắng rule stylesheet thường |
!important in stylesheet | Chuyển declaration sang bucket author important |
!important inline | Thắng !important trong stylesheet |
Quy tắc production: coi
!importantlà dấu hiệu thiết kế thất bại, không phải công cụ tinh chỉnh. Hãy dùng@layertrước.
Pseudo hàm: :is(), :not(), :where(), :has()
Selectors Level 4 thay đổi cách specificity hoạt động trong pseudo hàm. Đây là chỗ hầu hết senior dev vẫn bất ngờ.
:is() và :not() — max của các đối số
Pseudo lấy specificity cao nhất trong các đối số:
/* :is(.a, #b) → max((0,1,0), (1,0,0)) = (1,0,0)
plus span → (1, 0, 1) */
:is(.a, #b) span { }
/* :not(.disabled) → specificity of .disabled = (0, 1, 0) */
button:not(.disabled) { }
Trước :is(), viết #nav .item a, #nav .item button và nhân đôi phần prefix #nav .item. :is() cho phép gom nhóm mà không phình tuple quá worst case.
:where() — luôn bằng không
Mọi đối số trong :where() đóng góp (0, 0, 0), bất kể chúng trông specific thế nào:
/* :where(.x) → 0; .y → (0,1,0) → total (0, 1, 0) */
:where(.x) .y { }
/* Same structure, but :is() would be (1, 1, 0) */
:is(#sidebar) .y { }
Tại sao
:where()xuất sắc cho thư viện: ship reset hoặc base style không bao giờ chặn override của consumer. Style link design-system:where(a)giữ ở(0,0,1)— một class từ app thắng.
/* Library reset — zero class specificity cost */
@layer ds.base {
:where(a) {
color: inherit;
text-decoration: underline;
}
}
/* App override — trivial */
@layer app.components {
.prose-link { color: var(--accent); text-decoration: none; }
}
:has() — max của đối số (như :is())
:has() dùng cùng quy tắc max-argument như :is():
/* max((0,1,0), (0,0,1)) = (0,1,0) for the :has() part */
.card:has(.badge, img) { }
/* Parent selection — specificity includes :has() argument */
form:has(input:invalid) { border-color: var(--error); }
/* :has(input:invalid) → max of input:invalid = (0, 2, 1)
plus form → (0, 2, 2) */
Cascade Layers (@layer)
@layer thêm một bước cascade mới giữa origin và specificity. Hãy coi layer là làn ưu tiên rõ ràng — không thay thế selector tốt, mà là guardrail cấu trúc.
Khai báo thứ tự layer
Khai báo đầu tiên tên layer đặt thứ tự toàn cục. Layer sau thắng layer trước với declaration thường:
@layer reset, base, components, utilities;
@layer reset {
*, *::before, *::after { box-sizing: border-box; margin: 0; }
}
@layer base {
body { font-family: var(--font-mono); line-height: 1.5; }
a { color: var(--accent); }
}
@layer components {
.card { padding: 1rem; border: 1px solid var(--border); }
.btn { background: var(--accent); color: var(--bg); }
}
@layer utilities {
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; }
.text-center { text-align: center; }
}
Layer vs specificity — kịch bản demo
Xét ba rule trên cùng element:
@layer base, components, utilities;
@layer base {
.widget { color: gray; } /* (0, 1, 0) */
}
@layer components {
#target.widget { color: blue; } /* (1, 1, 0) — highest specificity */
}
@layer utilities {
.highlight { color: red; } /* (0, 1, 0) — lowest specificity */
}
Với thứ tự mặc định, utilities thắng → đỏ. Selector ID trong components không bao giờ được so specificity — bước layer đã quyết định rồi.
Style không layer
Mọi rule ngoài @layer thắng mọi declaration thường có layer, bất kể specificity:
@layer components {
#app .btn { color: blue; }
}
/* This wins — unlayered */
#app .btn { color: amber; }
Cảnh báo: vô tình để rule mới không layer là footgun migration phổ biến. Thiết lập convention: mọi thứ vào layer, hoặc cố ý dùng một file “overrides” không layer.
Layer lồng nhau
Layer có thể lồng. Thứ tự trong được giải quyết trước, rồi block lồng tham gia thứ tự parent:
@layer framework, app;
@layer framework {
@layer reset, components;
@layer reset {
:where(h1, h2, h3) { margin: 0; font-size: inherit; }
}
@layer components {
.f-card { border-radius: 8px; }
}
}
@layer app {
@layer overrides;
@layer overrides {
.f-card { border-radius: 0; } /* beats framework.components — app > framework */
}
}
Import vào layer
Tách CSS qua nhiều file mà không mất gán layer:
@layer reset, base, components, utilities;
@import url("reset.css") layer(reset);
@import url("tokens.css") layer(base);
@import url("components.css") layer(components);
@import url("utilities.css") layer(utilities);
Mỗi file import tự bọc rule trong layer đã đặt tên. Đây là cách design system lớn ship một CSS bundle với hành vi override dự đoán được.
!important đảo thứ tự ưu tiên layer
Với declaration important, layer trước thắng layer sau — ngược với rule thường:
@layer base, utilities;
@layer base {
.text { color: gray !important; } /* WINS over utilities !important */
}
@layer utilities {
.text { color: red !important; }
}
Đảo ngược này tồn tại để layer ưu tiên thấp vẫn thể hiện reset cứng bằng !important mà utilities không mất lợi thế cascade thường.
| Declaration type | Layer priority |
|---|---|
| Normal | Layer sau thắng |
!important | Layer trước thắng |
Mẫu kiến trúc thực tế
Mẫu 1: Stack layer design-system
@layer ds.reset, ds.tokens, ds.components, app.base, app.features, app.utilities;
| Layer | Owns | Specificity budget |
|---|---|---|
ds.reset | :where() resets, box-sizing | (0, 0, 0)–(0, 0, 1) |
ds.components | .ds-btn, .ds-card | (0, 1, 0) |
app.features | Page-specific blocks | (0, 1, 0)–(0, 2, 0) |
app.utilities | Tailwind-like helpers | (0, 1, 0) |
Team app không bao giờ đánh nhau với design system về specificity — họ tranh ở vị trí layer, rõ ràng và grep được.
Mẫu 2: :where() cô lập third-party
Khi bọc widget vendor, scope style bằng :where() để class app host luôn thắng trong cùng layer:
@layer vendor {
:where(.vendor-root) :where(button) {
font: inherit;
border: 1px solid var(--border);
}
}
Mẫu 3: Thuần hóa CSS legacy
Migrate codebase 50k dòng không big bang:
@layer legacy, app;
/* Move existing stylesheet wholesale */
@import url("legacy.css") layer(legacy);
@layer app {
/* New code — always wins over legacy */
.header { display: flex; }
}
Không rewrite selector ngày đầu. Code mới trong layer app thắng về cấu trúc. Refactor selector legacy dần dần.
Mẫu 4: Tránh cuộc đua vũ trang specificity
| Smell | Fix |
|---|---|
#page #content .sidebar .nav li a.active | Flatten to .nav-link--active in a higher layer |
Chains of !important | Introduce @layer utilities |
| ID selectors for styling | Replace with classes; IDs for JS hooks only |
| Duplicated long selectors | Use :is() to group, not repeat prefixes |
Takeaway cấp Principal: mục tiêu không phải “specificity thấp mọi nơi”. mục tiêu là đường override dự đoán được — layer cho cấu trúc,
:where()cho default không tốn specificity, class cho hook có chủ đích.
Debug xung đột Cascade
Khi style “lẽ ra” áp dụng nhưng không, duyệt bốn bước trong DevTools:
- Styles panel → kiểm tra property bị gạch ngang
- Click rule thắng → đọc badge layer (Chrome 99+)
- So specificity trong tooltip
- Cuộn xem rule sau cùng layer + specificity có thắng không
/* Quick isolation trick — temporary unlayered probe */
/* DELETE BEFORE COMMIT */
.debug-probe { outline: 2px solid red !important; }
Nếu probe chạy nhưng rule layered không, bạn có bug thứ tự layer, không phải bug specificity.
Hỗ trợ trình duyệt (2026)
| Feature | Chrome | Firefox | Safari | Notes |
|---|---|---|---|---|
@layer | 99+ | 97+ | 15.4+ | Production-ready |
:is() / :where() | 88+ | 78+ | 14+ | Baseline |
:has() | 105+ | 121+ | 15.4+ | Baseline 2023 |
:not() max-specificity | 88+ | 82+ | 14+ | Level 4 behavior |
Mọi tính năng trong bài an toàn cho app production hiện đại năm 2026. Với IE legacy, không áp dụng — nhưng bạn đã biết.
Tóm tắt
Cascade là cây quyết định bốn bước: origin/importance → layer → specificity → thứ tự nguồn. Specificity là bộ (a, b, c) — ID, rồi class/attribute/pseudo, rồi type/pseudo-element. :is(), :not(), :is(), :not(), và :has() lấy max đối số; :where() luôn bằng không.
@layer giúp kết thúc cuộc chiến specificity bằng kiến trúc, không phải đua vũ khí. Khai báo thứ tự một lần, import vào layer, giữ reset zero-cost với :where(), và không bao giờ để rule không layer vô tình trên production.
Dùng demo trên để xây trực giác trước thread Slack “sao style không apply?” lần tới.