Bài 2.2: Selectors và specificity trong CSS

Chào mừng các bạn quay trở lại với chuỗi bài blog về Lập trình Web Front-end! Nếu HTML là bộ xương tạo nên cấu trúc trang web, thì CSS chính là linh hồn mang lại vẻ đẹp và phong cách. Nhưng làm thế nào để nói cho CSS biết bạn muốn áp dụng kiểu dáng đó cho phần tử nào? Và khi có nhiều quy tắc CSS cùng tranh giành để định hình một phần tử, quy tắc nào sẽ thắng cuộc?

Đó chính là lúc Selectors (Bộ chọn)Specificity (Tính đặc hiệu) phát huy sức mạnh của mình. Nắm vững hai khái niệm này là bí quyết để bạn viết CSS hiệu quả, dễ quản lý và tránh được những tình huống "ủa sao CSS của mình không chạy?".

Hãy cùng nhau khám phá nào!

Selectors (Bộ chọn) - Ai là người được chọn?

Selectors là những "mẫu" mà bạn sử dụng trong CSS để chọn ra một hoặc nhiều phần tử HTML mà bạn muốn áp dụng kiểu dáng. Giống như việc bạn chỉ đích danh một người hoặc một nhóm người trong đám đông vậy.

Có rất nhiều loại Selector trong CSS, từ đơn giản đến phức tạp, giúp bạn linh hoạt nhắm mục tiêu vào các phần tử dựa trên: tên thẻ, class, ID, thuộc tính, vị trí trong cấu trúc, trạng thái,...

Dưới đây là các loại Selector phổ biến và cách sử dụng chúng:

1. Universal Selector (*)

Selector đơn giản nhất, nó chọn tất cả các phần tử HTML trên trang.

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

Giải thích: Quy tắc này thường được dùng để reset (đặt lại) marginpadding mặc định của trình duyệt cho tất cả các phần tử, cũng như thiết lập box-sizing sang border-box để việc tính toán kích thước dễ dàng hơn.

2. Type Selector (Element Selector)

Chọn tất cả các phần tử dựa trên tên thẻ HTML của chúng.

h1 {
  color: blue;
}

p {
  font-size: 16px;
  line-height: 1.5;
}
<h1>Tiêu đề chính</h1>
<p>Đoạn văn bản.</p>

Giải thích: Mọi thẻ <h1> trên trang sẽ có màu chữ xanh dương, và mọi thẻ <p> sẽ có cỡ chữ 16px và khoảng cách dòng 1.5 lần kích thước chữ.

3. Class Selector (.class-name)

Chọn tất cả các phần tử có thuộc tính class chứa tên class cụ thể. Bạn có thể áp dụng cùng một class cho nhiều phần tử.

.red-text {
  color: red;
}

.highlight {
  background-color: yellow;
}
<p class="red-text">Đoạn văn bản màu đỏ.</p>
<h2 class="highlight">Tiêu đề được làm nổi bật.</h2>
<p class="red-text highlight">Đoạn văn bản màu đỏ và được làm nổi bật.</p>

Giải thích: Các phần tử có class red-text sẽ có màu chữ đỏ. Các phần tử có class highlight sẽ có nền vàng. Một phần tử có thể có nhiều class, và nó sẽ nhận các kiểu dáng từ tất cả các class đó.

4. ID Selector (#id-name)

Chọn phần tử duy nhất có thuộc tính id khớp với tên ID cụ thể. Thuộc tính id nên là duy nhất trên một trang HTML.

#main-header {
  font-size: 3em; /* Gấp 3 lần kích thước font cơ bản */
  text-align: center;
}
<h1 id="main-header">Tiêu đề duy nhất của trang</h1>
<p>Đoạn văn bản khác.</p>
<!-- KHÔNG NÊN có một phần tử khác với id="main-header" -->

Giải thích: Chỉ có phần tử duy nhất có id="main-header" mới nhận được kiểu dáng này. ID Selector có tính đặc hiệu cao (sẽ nói rõ hơn ở phần Specificity).

5. Attribute Selector ([attribute], [attribute="value"], etc.)

Chọn các phần tử dựa trên sự tồn tại của một thuộc tính hoặc giá trị của thuộc tính đó.

[target] {
  border: 1px solid black; /* Chọn tất cả các thẻ có thuộc tính target */
}

[type="submit"] {
  background-color: green; /* Chọn tất cả các thẻ input/button có type="submit" */
  color: white;
}

a[href^="https://"] {
  color: purple; /* Chọn các thẻ a có href bắt đầu bằng "https://" */
}
<a href="#" target="_blank">Mở liên kết trong tab mới</a>
<button type="submit">Gửi</button>
<a href="https://fullhousedev.com">Website</a>
<a href="http://example.com">Liên kết khác</a>

Giải thích: Quy tắc đầu tiên áp dụng viền cho thẻ <a>. Quy tắc thứ hai biến nút "Gửi" thành màu xanh lá. Quy tắc thứ ba biến các liên kết bắt đầu bằng https:// thành màu tím.

6. Group Selector (selector1, selector2)

Áp dụng cùng một bộ quy tắc CSS cho nhiều Selector khác nhau bằng cách liệt kê chúng và phân cách bởi dấu phẩy.

h1, h2, h3 {
  color: #333; /* Áp dụng màu xám đậm cho h1, h2, và h3 */
  margin-bottom: 15px;
}

Giải thích: Thay vì viết cùng một bộ quy tắc cho h1, rồi lại cho h2, rồi lại cho h3, bạn có thể nhóm chúng lại để code gọn gàng hơn.

7. Combinators (Bộ kết hợp)

Combinators cho phép bạn chọn các phần tử dựa trên mối quan hệ của chúng với các phần tử khác trong cấu trúc HTML (cây DOM).

  • Descendant Selector (selector1 selector2): Chọn selector2 nằm bên trong selector1, bất kể cấp độ sâu nào.

    article p {
      font-style: italic; /* Chọn tất cả các thẻ p nằm trong thẻ article */
    }
    
    <article>
      <p>Đoạn 1 (trong article).</p>
      <div>
        <p>Đoạn 2 (cũng trong article, dù có div ở giữa).</p>
      </div>
    </article>
    <p>Đoạn 3 (ngoài article).</p>
    

    Giải thích: Cả "Đoạn 1" và "Đoạn 2" đều sẽ in nghiêng vì chúng là con cháu của <article>. "Đoạn 3" thì không.

  • Child Selector (selector1 > selector2): Chọn selector2con trực tiếp của selector1.

    ul > li {
      list-style: square; /* Chỉ chọn các thẻ li là con trực tiếp của ul */
    }
    
    <ul>
      <li>Item 1 (con trực tiếp)</li>
      <li>Item 2 (con trực tiếp)
        <ul>
          <li>Sub-item 1 (không phải con trực tiếp của ul gốc)</li>
        </ul>
      </li>
    </ul>
    

    Giải thích: "Item 1" và "Item 2" sẽ có dấu chấm vuông. "Sub-item 1" sẽ không, vì nó là con của thẻ <ul> bên trong, chứ không phải thẻ <ul> gốc.

  • Adjacent Sibling Selector (selector1 + selector2): Chọn selector2anh em liền kề ngay sau selector1.

    h2 + p {
      margin-top: 0; /* Chọn thẻ p ngay sau h2 */
    }
    
    <h2>Tiêu đề</h2>
    <p>Đoạn văn bản ngay sau tiêu đề.</p>
    <p>Đoạn văn bản khác, không liền kề h2.</p>
    

    Giải thích: Chỉ đoạn văn bản "Đoạn văn bản ngay sau tiêu đề." mới nhận được kiểu dáng margin-top: 0.

  • General Sibling Selector (selector1 ~ selector2): Chọn selector2anh em đứng sau selector1, không cần liền kề.

    h2 ~ p {
      color: grey; /* Chọn tất cả các thẻ p là anh em và đứng sau h2 */
    }
    
    <h2>Tiêu đề</h2>
    <p>Đoạn văn bản thứ nhất (anh em sau h2).</p>
    <div>Một thẻ khác</div>
    <p>Đoạn văn bản thứ hai (cũng là anh em sau h2).</p>
    

    Giải thích: Cả hai đoạn văn bản đều nhận màu xám vì chúng là anh em của <h2> và đứng sau <h2>.

8. Pseudo-classes (:state) và Pseudo-elements (::part)
  • Pseudo-classes: Chọn các phần tử dựa trên trạng thái của chúng (ví dụ: khi di chuột qua, khi được click, khi là phần tử con đầu tiên/cuối cùng) hoặc vị trí trong cấu trúc (nhưng không phải là mối quan hệ trực tiếp).

    a:hover {
      text-decoration: underline; /* Khi di chuột qua liên kết */
    }
    
    input:focus {
      border-color: blue; /* Khi input được focus */
    }
    
    li:first-child {
      font-weight: bold; /* Phần tử li đầu tiên trong nhóm anh em */
    }
    
    p:not(.intro) {
      margin-top: 20px; /* Tất cả các thẻ p KHÔNG có class "intro" */
    }
    

    Giải thích: Các pseudo-classes cho phép bạn áp dụng kiểu dáng dựa trên tương tác của người dùng hoặc vị trí động của phần tử. :not() là một pseudo-class đặc biệt để loại trừ các phần tử khớp với selector bên trong.

  • Pseudo-elements: Chọn và tạo kiểu cho các phần của một phần tử mà không có trong cấu trúc HTML (ví dụ: ký tự đầu tiên, dòng đầu tiên, hoặc nội dung được tạo ra trước/sau phần tử).

    p::first-line {
      font-variant: small-caps; /* Áp dụng cho dòng đầu tiên của mỗi đoạn văn */
    }
    
    h2::before {
      content: "-> "; /* Chèn ký tự "->" trước mỗi thẻ h2 */
      color: green;
    }
    

    Giải thích: ::first-line chỉ áp dụng kiểu dáng cho dòng chữ đầu tiên. ::before::after tạo ra các "hộp" nội dung ảo (có thể chèn chữ, hình ảnh nhỏ,...) trước hoặc sau nội dung thực của phần tử được chọn. Lưu ý sử dụng hai dấu hai chấm (::) cho pseudo-elements theo chuẩn hiện đại (dù một dấu : vẫn hoạt động cho các pseudo-elements cũ).

Bạn thấy đấy, Selectors mang lại cho chúng ta một "kho vũ khí" đa dạng để nhắm đúng mục tiêu!

Specificity (Tính đặc hiệu) - Ai sẽ thắng cuộc?

Bây giờ, vấn đề phát sinh khi một phần tử HTML duy nhất lại bị nhắm mục tiêu bởi nhiều quy tắc CSS khác nhau. Ví dụ:

<p class="text" id="main-text">Đoạn văn bản quan trọng.</p>

Và bạn có các quy tắc CSS sau:

p {
  color: red; /* Quy tắc 1 */
}

.text {
  color: blue; /* Quy tắc 2 */
}

#main-text {
  color: green; /* Quy tắc 3 */
}

Đoạn văn bản này cuối cùng sẽ có màu gì? Đỏ, xanh dương, hay xanh lá cây?

Đây chính là lúc Specificity bước vào sân khấu. Specificity là thuật toán mà trình duyệt sử dụng để xác định quy tắc CSS nào là đặc hiệu nhất (quan trọng nhất) và sẽ được áp dụng khi có nhiều quy tắc xung đột nhau nhắm vào cùng một phần tử.

Trình duyệt tính Specificity dựa trên một hệ thống "điểm" (hay chính xác hơn là các cấp độ ưu tiên được sắp xếp từ cao đến thấp). Hãy tưởng tượng Specificity được tính trong 4 cột: (A, B, C, D).

  • A: Inline Styles: Nếu có kiểu dáng được viết trực tiếp trong thuộc tính style của thẻ HTML (<p style="color: orange;">), nó có ưu tiên cao nhất. Mỗi khai báo inline style được tính 1 ở cột A (1,0,0,0).
  • B: IDs: Mỗi ID Selector (#myID) được tính 1 ở cột B (0,1,0,0).
  • C: Classes, Attributes, Pseudo-classes: Mỗi Class Selector (.myClass), Attribute Selector ([type="text"]), và Pseudo-class (:hover, :first-child) được tính 1 ở cột C (0,0,1,0).
  • D: Elements và Pseudo-elements: Mỗi Type Selector (p, div) và Pseudo-element (::before, ::after) được tính 1 ở cột D (0,0,0,1).
  • Universal Selector (*): Có Specificity thấp nhất (0,0,0,0).
  • Combinators (+, >, ~, khoảng trắng), !important: Combinators không thêm điểm Specificity. Thuộc tính !important thì lại phá vỡ hệ thống Specificity thông thường và cần được xem xét riêng (sẽ nói sau).

Cách tính Specificity:

Bạn đếm số lần xuất hiện của mỗi loại Selector trong một quy tắc CSS và xếp chúng vào các cột tương ứng. Sau đó so sánh các "số" này từ trái sang phải (A, B, C, D). Quy tắc có "số" lớn hơn ở cột ưu tiên hơn sẽ thắng.

Lưu ý quan trọng: Các cột không cộng dồn theo kiểu thập phân thông thường. Ví dụ: 10 Class Selectors (.class.class... 10 lần) có Specificity là (0,0,10,0), trong khi 1 ID Selector (#myID) có Specificity là (0,1,0,0). Specificity của ID (0,1,0,0) luôn cao hơn Specificity của 10 Class (0,0,10,0) vì cột B có ưu tiên cao hơn cột C.

Ví dụ minh họa Specificity:

Quay lại ví dụ ban đầu:

p { /* Specificity: (0,0,0,1) - 1 Element */
  color: red; /* Quy tắc 1 */
}

.text { /* Specificity: (0,0,1,0) - 1 Class */
  color: blue; /* Quy tắc 2 */
}

#main-text { /* Specificity: (0,1,0,0) - 1 ID */
  color: green; /* Quy tắc 3 */
}

So sánh Specificity:

  • Quy tắc 1: (0,0,0,1)
  • Quy tắc 2: (0,0,1,0)
  • Quy tắc 3: (0,1,0,0)

So sánh từ trái sang phải:

  • Cột A: Tất cả đều là 0.
  • Cột B: Quy tắc 3 có 1, các quy tắc khác là 0. Quy tắc 3 thắng cuộc ở cột B.

Kết quả: Quy tắc 3 (#main-text) có Specificity cao nhất (0,1,0,0). Do đó, đoạn văn bản sẽ có màu xanh lá cây.

Một vài ví dụ khác:

/* Ví dụ 1: Class vs Element */
div { /* Specificity: (0,0,0,1) */
  border: 1px solid black;
}
.container { /* Specificity: (0,0,1,0) */
  border: 2px solid red;
}
<div class="container">Nội dung</div>

Giải thích: .container (0,0,1,0) có Specificity cao hơn div (0,0,0,1) (vì 1 ở cột C > 0 ở cột C), nên phần tử sẽ có viền màu đỏ dày 2px.

/* Ví dụ 2: Nhiều Class vs một ID */
.box.wide { /* Specificity: (0,0,2,0) - 2 Classes */
  background-color: yellow;
}
#promo-box { /* Specificity: (0,1,0,0) - 1 ID */
  background-color: orange;
}
<div id="promo-box" class="box wide">Nội dung khuyến mãi.</div>

Giải thích: #promo-box (0,1,0,0) có Specificity cao hơn .box.wide (0,0,2,0) (vì 1 ở cột B > 0 ở cột B). Phần tử sẽ có nền màu cam.

/* Ví dụ 3: Descendant vs Class */
#header p { /* Specificity: (0,1,0,1) - 1 ID, 1 Element */
  font-size: 18px;
}
.article-text { /* Specificity: (0,0,1,0) - 1 Class */
  font-size: 16px;
}
<header id="header">
  <p class="article-text">Đoạn giới thiệu.</p>
</header>

Giải thích: #header p (0,1,0,1) có Specificity cao hơn .article-text (0,0,1,0) (vì 1 ở cột B > 0 ở cột B). Đoạn văn bản sẽ có cỡ chữ 18px.

!important - Vị vua "chuyên quyền"

Thuộc tính !important được thêm vào cuối một giá trị CSS (property: value !important;). Nó có sức mạnh ưu tiên hơn cả Specificity thông thường (nhưng vẫn kém hơn inline styles có !important - trường hợp này hiếm gặp và nên tránh).

p { /* Specificity: (0,0,0,1) */
  color: red;
}

.text { /* Specificity: (0,0,1,0) */
  color: blue !important; /* THẮNG VÌ CÓ !important */
}

#main-text { /* Specificity: (0,1,0,0) */
  color: green;
}
<p class="text" id="main-text">Đoạn văn bản quan trọng.</p>

Giải thích: Mặc dù #main-text có Specificity cao nhất theo cách tính thông thường (0,1,0,0), nhưng .text lại có !important cho thuộc tính color. Do đó, màu chữ của đoạn văn bản sẽ là xanh dương.

Sử dụng !important giống như việc bạn hét lên để được chú ý. Nó có thể hữu ích trong một số trường hợp đặc biệt (ví dụ: ghi đè CSS từ thư viện bên ngoài mà bạn không thể sửa), nhưng nên hạn chế sử dụng tối đa. Nó làm cho CSS khó quản lý và debug hơn vì phá vỡ dòng chảy Specificity tự nhiên. Hãy cố gắng giải quyết xung đột bằng cách viết Selector có Specificity phù hợp hơn thay vì dùng !important.

Specificity bằng nhau? Ai đến trước ăn trước!

Nếu hai hoặc nhiều quy tắc CSS có cùng Specificity nhắm vào cùng một phần tử và thuộc tính, thì quy tắc được khai báo sau cùng trong stylesheet (hoặc được tải sau) sẽ thắng cuộc. Đây là nguyên tắc Order of Appearance (Thứ tự xuất hiện) trong CSS Cascade.

.box { /* Specificity: (0,0,1,0) */
  background-color: red; /* Khai báo trước */
}

.box { /* Specificity: (0,0,1,0) */
  background-color: blue; /* Khai báo sau, Specificity BẰNG NHUA */
}
<div class="box">Hộp màu gì đây?</div>

Giải thích: Cả hai quy tắc đều nhắm vào class .box và đều có Specificity (0,0,1,0). Vì quy tắc thứ hai được khai báo sau, nó sẽ ghi đè quy tắc thứ nhất. Hộp sẽ có nền màu xanh dương.

Tổng kết

Selectors và Specificity là hai mặt của cùng một đồng xu trong CSS.

  • Selectors giúp bạn chọn ra những phần tử cần áp dụng kiểu dáng.
  • Specificity là luật chơi, quy định quy tắc nào thắng khi có sự tranh chấp.

Hiểu rõ cách Selectors hoạt động và cách Specificity được tính toán là cực kỳ quan trọng để bạn có thể viết CSS chính xác, dự đoán được kết quả và dễ dàng sửa lỗi khi có vấn đề về kiểu dáng. Thay vì dùng !important một cách bừa bãi, hãy học cách xây dựng các Selector có Specificity phù hợp với cấu trúc HTML của bạn.

Chúc mừng bạn đã vượt qua một cột mốc quan trọng trong hành trình làm chủ CSS! Hãy thực hành thật nhiều với các loại Selectors khác nhau và thử tính Specificity để củng cố kiến thức nhé.

Comments

There are no comments at the moment.