Mở đầu — vì sao bài này quan trọng
Bạn đã từng rơi vào tình huống này chưa: bạn viết một dòng CSS để đổi màu nút bấm thành xanh, lưu file, reload trình duyệt... và nút vẫn cứng đầu giữ nguyên màu xám cũ? Bạn kiểm tra lại tên class, đúng. Cú pháp, đúng. File đã được nạp, đúng. Nhưng nó vẫn không chịu đổi. Cuối cùng, trong tuyệt vọng, bạn thêm !important vào — và nó hoạt động. Bạn thở phào, nhưng trong lòng vẫn lấn cấn: "Tại sao mình phải dùng cái đó? Rốt cuộc chuyện gì đang xảy ra?"
Câu trả lời nằm ở hai khái niệm mà bài học hôm nay sẽ mổ xẻ: selectors (bộ chọn) và specificity (độ ưu tiên). Đây là cặp khái niệm quyết định: khi có nhiều quy tắc CSS cùng nhắm vào một phần tử, trình duyệt sẽ nghe lời quy tắc nào.
Nếu ở Bài 2 bạn đã làm quen với những điều cơ bản của CSS, thì bài này là lúc bạn hiểu cơ chế "phân xử" bên trong trình duyệt. Đây không phải kiến thức học thuộc cho vui — nó là thứ phân biệt một người "viết CSS được" với một người "kiểm soát được CSS". Khi bạn nắm chắc specificity, bạn sẽ ngừng đoán mò, ngừng lạm dụng !important, và viết được CSS sạch, dễ bảo trì cho cả dự án lớn. Một codebase CSS hỗn loạn ở các công ty thường bắt nguồn từ chính việc lập trình viên không hiểu chương này.
Khái niệm cốt lõi
Selector là gì
Selector là phần nằm trước dấu { trong một quy tắc CSS — nó "chỉ tay" vào những phần tử HTML mà bạn muốn tạo kiểu. Trong đoạn dưới đây, .btn chính là selector:
.btn {
background: blue;
}
Có rất nhiều loại selector, nhưng với người mới, ba loại nền tảng quan trọng nhất là: type, class, và ID.
Type selector (còn gọi là element selector) nhắm vào tên thẻ HTML. Ví dụ p nhắm vào mọi đoạn văn, div nhắm vào mọi thẻ div, h1 nhắm vào mọi tiêu đề cấp một.
p { color: #333; }
Class selector bắt đầu bằng dấu chấm ., nhắm vào mọi phần tử có thuộc tính class tương ứng. Đây là loại bạn sẽ dùng nhiều nhất trong thực tế vì nó tái sử dụng được.
.card-title { font-size: 20px; }
ID selector bắt đầu bằng dấu thăng #, nhắm vào phần tử duy nhất mang id đó. Theo chuẩn HTML, mỗi id chỉ được xuất hiện một lần trên trang.
#main-header { height: 64px; }
Ngoài ba loại trên, bạn còn gặp attribute selector ([type="email"]), pseudo-class (:hover, :focus, :first-child), và pseudo-element (::before, ::after). Chúng ta sẽ nhắc tới khi bàn về cách tính điểm specificity.
Specificity — quy tắc phân xử
Đây là phần cốt lõi của bài. Specificity là một con số mà trình duyệt tính cho mỗi selector để quyết định selector nào "thắng" khi nhiều quy tắc cùng tác động lên một phần tử và cùng đặt một thuộc tính.
Cách dễ nhớ nhất là hình dung specificity như một bộ ba con số dạng (a, b, c):
- a = số lượng ID selector
- b = số lượng class, attribute, và pseudo-class selector
- c = số lượng type (element) và pseudo-element selector
Hãy xem điểm của vài selector cụ thể:
| Selector | a (ID) | b (class) | c (type) | Đọc là |
|---|---|---|---|---|
p | 1 | (0,0,1) | ||
.btn | 1 | (0,1,0) | ||
#header | 1 | (1,0,0) | ||
.nav .btn | 2 | (0,2,0) | ||
ul li a | 3 | (0,0,3) | ||
a:hover | 1 | 1 | (0,1,1) | |
#nav .btn span | 1 | 1 | 1 | (1,1,1) |
(1,0,0) luôn thắng bất kỳ tổ hợp class nào. Dù bạn có chồng .a .b .c .d .e .f .g .h .i .j .k thành điểm (0,11,0), nó vẫn thua một #x đơn lẻ có điểm (1,0,0). Vì so sánh ưu tiên cột ID trước, và cột đó 1 > 0. Đây chính là lý do nhiều chuyên gia khuyên hạn chế dùng ID để tạo style — nó tạo ra những "bức tường" specificity mà về sau rất khó ghi đè.Khi specificity bằng nhau: luật "ai đến sau thắng"
Nếu hai selector có specificity hoàn toàn giống nhau, trình duyệt áp dụng nguyên tắc source order — quy tắc nào được viết sau trong file (hoặc nạp sau) sẽ thắng.
.btn { color: red; }
.btn { color: green; } / Nút sẽ màu xanh lá, vì đứng sau /
Đây là lý do thứ tự nạp các file CSS rất quan trọng, và là cơ sở để các framework hoạt động đúng.
Bậc thang ưu tiên đầy đủ
Ngoài ba cấp trên, còn có hai "tầng" đặc biệt nằm trên cùng:
- Inline style (viết thẳng vào thuộc tính
style=""của thẻ HTML) — mạnh hơn mọi selector trong file CSS, tương đương điểm(1,0,0,0). !important— ghi đè lên tất cả những thứ trên. Đây là "vũ khí hạt nhân", và như mọi vũ khí hạt nhân, dùng bừa bãi thì chính bạn lãnh hậu quả.
!important.Tình huống thực tế
Ví dụ 1: Nút "Đặt hàng" cứng đầu ở một sàn TMĐT
Một bạn dev frontend ở team của Tiki được giao việc đổi màu nút "Thêm vào giỏ" trên trang sản phẩm từ xanh dương sang đỏ cho chiến dịch Flash Sale. Bạn ấy mở file CSS riêng của trang, viết:
.add-to-cart { background: #e53935; } / (0,1,0) /
Reload — nút vẫn xanh. Bạn ấy bực bội, mở DevTools (công cụ trình duyệt) ra, vào tab Styles, và thấy quy tắc của mình bị gạch ngang. Phía trên nó là một dòng từ file CSS gốc của hệ thống:
#product-detail .add-to-cart { background: #1976d2; } / (1,1,0) /
À há. Quy tắc gốc có một ID là #product-detail nên điểm là (1,1,0), trong khi quy tắc mới chỉ (0,1,0). Cột ID 1 > 0, nên quy tắc gốc thắng tuyệt đối, dù nó được viết trước.
Bài học rút ra: Bạn ấy có hai lựa chọn. Cách sai (nhưng nhanh): thêm !important. Cách đúng: tăng specificity một cách có chủ đích để vượt qua, ví dụ viết #product-detail .add-to-cart { background: #e53935; } đặt sau quy tắc gốc. Tốt nhất về lâu dài là đề xuất team gỡ ID #product-detail khỏi selector để hệ thống không còn những "bức tường" như vậy.
Ví dụ 2: Cuộc chiến !important ở một startup Việt
Một startup edtech ở TP.HCM có một codebase CSS đã 3 năm tuổi, qua tay 6 lập trình viên. Khi một bạn mới vào nhận task chỉnh khoảng cách padding của thẻ card, bạn ấy đếm được trong file có tới 240 lần dùng !important. Mỗi khi ai đó không ghi đè được style, họ lại thêm một !important mới. Hậu quả: giờ đây để ghi đè một !important, bạn buộc phải dùng một !important khác có specificity cao hơn — một cuộc chạy đua vũ trang không có hồi kết.
Bạn ấy thử đổi padding: 16px cho .card nhưng không ăn thua, vì đâu đó có dòng .card { padding: 8px !important; }. Để sửa, bạn buộc phải viết div.card { padding: 16px !important; } — vừa thêm type selector vừa thêm !important. Code ngày càng xấu.
Bài học rút ra: !important không "miễn phí" — nó là khoản nợ kỹ thuật. Mỗi lần dùng, bạn đẩy gánh nặng sang người sau. Quy ước lành mạnh ở các team chuyên nghiệp: chỉ cho phép !important ở những trường hợp cực hạn như utility class dùng một mục đích duy nhất (ví dụ .hidden { display: none !important; }), còn lại tuyệt đối cấm. Team này sau đó dành hẳn một sprint để "trả nợ", gỡ dần !important và làm phẳng specificity.
Ví dụ 3: Vì sao Tailwind cố tình giữ specificity thấp
Bạn có để ý các framework CSS hiện đại như Tailwind (Bài 40 sẽ học sâu) đều dùng class chứ không bao giờ dùng ID không? Đó là một quyết định kiến trúc có chủ đích. Tailwind tạo ra hàng nghìn class tiện ích như .p-4, .text-red-500, .flex, và mỗi class chỉ có điểm (0,1,0). Vì tất cả đều cùng một mức specificity, thứ tự áp dụng trở nên dễ dự đoán — quy tắc nào xuất hiện sau trong file CSS đã build sẽ thắng, không có chuyện một ID bất ngờ "đè bẹp" cả chục class của bạn.
Một công ty agency ở Hà Nội chuyển dự án landing page từ CSS viết tay sang Tailwind đã ghi nhận thời gian xử lý bug giao diện giảm rõ rệt, đơn giản vì lập trình viên không còn phải "đấu specificity" nữa.
Bài học rút ra: Giữ specificity phẳng và thấp không phải là hạn chế — đó là một chiến lược giúp CSS dễ đoán và dễ bảo trì ở quy mô lớn.
Hướng dẫn từng bước
Đây là quy trình bạn nên áp dụng mỗi khi gặp một style "không chịu áp dụng":
- Mở DevTools. Nhấn chuột phải vào phần tử, chọn "Inspect". Vào tab Styles (Chrome) hoặc Rules (Firefox).
- Tìm thuộc tính bị gạch ngang. Trình duyệt gạch ngang (strikethrough) những khai báo bị ghi đè. Đây là bằng chứng trực quan cho thấy quy tắc của bạn đang thua.
- Xác định selector đang thắng. Nhìn xem quy tắc nào đang được áp dụng (không bị gạch). Đọc selector của nó.
- Tính điểm specificity của cả hai. So sánh
(a,b,c)của selector bạn và selector đang thắng. Bạn sẽ thấy ngay ai cao hơn ở cột nào.
- Chọn cách giải quyết theo thứ tự ưu tiên:
(0,2,0), bạn dùng (0,2,0) đặt sau hoặc (0,3,0).
- Chấp nhận được: Giảm specificity của quy tắc đang gây cản trở (nếu bạn có quyền sửa nó) thay vì tăng của mình.
- Cuối cùng, hạn chế: Dùng !important chỉ cho utility class.- Tự kiểm tra bằng công cụ. Bạn có thể gõ selector vào trang [specificity.keegan.st](https://specificity.keegan.st) để nó tính điểm giúp, rất hữu ích khi mới học.
Lỗi thường gặp & mẹo
Lỗi 1 — Lạm dụng ID để tạo style. ID nên dành cho JavaScript (getElementById) và liên kết neo (#section), không phải để style. Một khi đã style bằng ID, bạn tạo ra điểm (1,0,0) mà sau này class nào cũng không vượt qua nổi. Mẹo: trong style sheet, ưu tiên class gần như tuyệt đối.
Lỗi 2 — Nghĩ rằng "nhiều class hơn = luôn thắng ID". Sai. (0,11,0) vẫn thua (1,0,0). Đừng so sánh tổng, hãy so sánh từng cột từ trái sang.
Lỗi 3 — Lồng selector quá sâu. Viết .header .nav .menu .item a cho ra (0,3,2) — một con số khó ghi đè không cần thiết. Mẹo: giữ selector càng phẳng càng tốt, lý tưởng là một class duy nhất cho mỗi component.
Lỗi 4 — Dùng !important để "vá nhanh". Nó luôn quay lại cắn bạn. Mẹo: nếu thấy mình muốn gõ !important, hãy dừng lại và kiểm tra specificity trước — 90% trường hợp có cách sạch hơn.
Mẹo cộng thêm — :where() để hạ specificity về 0. Selector hiện đại :where() có specificity bằng (0,0,0). Ví dụ :where(.card) .title chỉ tính điểm cho .title, còn .card được "miễn". Đây là công cụ mạnh để viết CSS nền (base styles) mà không gây khó ghi đè về sau.
Mẹo — đặt utility class sau cùng trong file CSS. Vì khi specificity bằng nhau thì "ai sau thắng", các class tiện ích như .mt-0, .hidden nên nằm cuối file để chúng dễ ghi đè các style component đặt trước.
Bài tập thực hành
- Tính điểm bằng tay. Tính specificity
(a,b,c)cho từng selector sau và xếp chúng từ yếu đến mạnh:a·.menu a·#nav a·nav ul li a:hover·.menu .item a. (Gợi ý: nhớ đếm pseudo-class:hovervào cột b.)
- Đoán kết quả. Cho đoạn HTML
<a class="link" id="home">Trang chủ</a>và CSS sau, hãy đoán màu cuối cùng của chữ rồi kiểm chứng trong trình duyệt:
a { color: black; }
.link { color: blue; }
#home { color: green; }
- Phá vỡ
!important. Cho.box { color: red !important; }, hãy viết một quy tắc khác làm cho chữ chuyển sang xanh dương mà không sửa dòng gốc. (Gợi ý: cần một!importantthứ hai có specificity cao hơn.)
- Tái cấu trúc. Lấy selector
#sidebar .widget .title h3và viết lại bằng một class duy nhất để hạ specificity xuống(0,1,0), đồng thời thêm class tương ứng vào HTML.
- Thử
:where(). Viết một base style cho liên kết bên trong.articlesao cho specificity vẫn chỉ tính cho thẻa, để sau này một class.featured-linkdễ dàng ghi đè.
Tóm tắt
- Selector là phần "chỉ tay" vào phần tử HTML; ba loại nền tảng là type
(0,0,1), class(0,1,0), và ID(1,0,0). - Specificity là bộ ba số
(a,b,c)đếm ID, class/attribute/pseudo-class, và type/pseudo-element. So sánh từ trái sang phải; cột bên trái luôn nặng hơn mọi thứ bên phải. - Một ID đơn lẻ luôn thắng mọi tổ hợp class — đây là lý do nên tránh style bằng ID.
- Khi specificity bằng nhau, quy tắc viết sau thắng (source order).
- Bậc thang đầy đủ: type < class < ID < inline style <
!important. !importantlà khoản nợ kỹ thuật — chỉ dùng cho utility class, đừng dùng để vá lỗi.- Giữ selector phẳng và thấp specificity là chiến lược giúp CSS dễ đoán, dễ bảo trì; đó cũng là triết lý của các framework như Tailwind.
- Công cụ của bạn: DevTools để thấy quy tắc bị gạch ngang, và
:where()để hạ specificity khi cần viết style nền.
!important sẽ không còn là "phép màu" bạn cầu cứu trong tuyệt vọng, mà là một lựa chọn bạn chủ động tránh. Đó là dấu hiệu bạn đã kiểm soát được CSS, chứ không để CSS kiểm soát mình.