Giới hạn phạm vi tiếp cận của bộ chọn bằng CSS @scope at-quy tắc

Tìm hiểu cách sử dụng @scope để chỉ chọn các phần tử trong một cây con giới hạn của DOM.

Xuất bản: Ngày 4 tháng 10 năm 2023

Browser Support

  • Chrome: 118.
  • Edge: 118.
  • Firefox: 146.
  • Safari: 17.4.

Source

Khi viết bộ chọn, bạn có thể thấy mình bị giằng xé giữa hai thế giới. Một mặt, bạn muốn chọn các phần tử khá cụ thể. Mặt khác, bạn muốn các bộ chọn của mình vẫn dễ dàng ghi đè và không bị liên kết chặt chẽ với cấu trúc DOM.

Ví dụ: khi muốn chọn "hình ảnh chính trong vùng nội dung của thành phần thẻ" (đây là một lựa chọn thành phần khá cụ thể), bạn có thể không muốn viết một bộ chọn như .card > .content > img.hero.

  • Bộ chọn này có độ đặc hiệu khá cao là (0,3,1), khiến bạn khó ghi đè khi mã của bạn tăng lên.
  • Bằng cách dựa vào bộ kết hợp phần tử con trực tiếp, bộ chọn này được liên kết chặt chẽ với cấu trúc DOM. Nếu mã đánh dấu thay đổi, bạn cũng cần thay đổi CSS.

Tuy nhiên, bạn cũng không muốn chỉ viết img làm bộ chọn cho phần tử đó, vì như vậy sẽ chọn tất cả các phần tử hình ảnh trên trang của bạn.

Việc tìm ra sự cân bằng phù hợp trong vấn đề này thường là một thách thức lớn. Trong những năm qua, một số nhà phát triển đã đưa ra các giải pháp và cách khắc phục để giúp bạn trong những tình huống như thế này. Ví dụ:

  • Các phương pháp như BEM quy định rằng bạn phải đặt cho phần tử đó một lớp card__img card__img--hero để giữ cho độ đặc hiệu thấp trong khi cho phép bạn chỉ định cụ thể những gì bạn chọn.
  • Các giải pháp dựa trên JavaScript như CSS có phạm vi hoặc Styled Components sẽ viết lại tất cả bộ chọn của bạn bằng cách thêm các chuỗi được tạo ngẫu nhiên (chẳng hạn như sc-596d7e0e-4) vào bộ chọn để ngăn bộ chọn nhắm đến các phần tử ở phía bên kia của trang.
  • Một số thư viện thậm chí còn loại bỏ hoàn toàn bộ chọn và yêu cầu bạn đặt các trình kích hoạt kiểu trực tiếp trong chính mã đánh dấu.

Nhưng nếu bạn không cần bất kỳ thông tin nào trong số đó thì sao? Điều gì sẽ xảy ra nếu CSS cho phép bạn chọn các phần tử một cách khá cụ thể mà không yêu cầu bạn viết các bộ chọn có độ đặc hiệu cao hoặc các bộ chọn được liên kết chặt chẽ với DOM? Đó là lúc @scope phát huy tác dụng, giúp bạn chỉ chọn các phần tử trong một cây con của DOM.

Giới thiệu về @scope

Với @scope, bạn có thể giới hạn phạm vi của bộ chọn. Bạn thực hiện việc này bằng cách đặt thư mục gốc của phạm vi. Thư mục này sẽ xác định ranh giới trên của cây con mà bạn muốn nhắm đến. Khi bạn đặt một gốc phạm vi, các quy tắc kiểu được chứa (được gọi là quy tắc kiểu có phạm vi) chỉ có thể chọn từ cây con hạn chế đó của DOM.

Ví dụ: để chỉ nhắm đến các phần tử <img> trong thành phần .card, bạn đặt .card làm gốc phạm vi của quy tắc @scope.

@scope (.card) {
    img {
        border-color: green;
    }
}

Quy tắc kiểu có phạm vi img { … } chỉ có thể chọn các phần tử <img> trong phạm vi của phần tử .card được so khớp một cách hiệu quả.

Để ngăn các phần tử <img> bên trong vùng nội dung của thẻ (.card__content) được chọn, bạn có thể làm cho bộ chọn img cụ thể hơn. Một cách khác để thực hiện việc này là sử dụng thực tế rằng quy tắc @scope cũng chấp nhận một giới hạn phạm vi xác định ranh giới dưới.

@scope (.card) to (.card__content) {
    img {
        border-color: green;
    }
}

Quy tắc kiểu có phạm vi này chỉ nhắm đến các phần tử <img> được đặt giữa các phần tử .card.card__content trong cây tổ tiên. Loại phạm vi này (có ranh giới trên và dưới) thường được gọi là phạm vi hình vành khăn

Bộ chọn :scope

Theo mặc định, tất cả các quy tắc về kiểu có phạm vi đều tương ứng với gốc phạm vi. Bạn cũng có thể nhắm đến chính phần tử gốc của phạm vi. Để làm việc này, hãy sử dụng bộ chọn :scope.

@scope (.card) {
    :scope {
        /* Selects the matched .card itself */
    }
    img {
       /* Selects img elements that are a child of .card */
    }
}

Bộ chọn bên trong các quy tắc kiểu có phạm vi sẽ được thêm :scope một cách ngầm định. Nếu muốn, bạn có thể nêu rõ điều này bằng cách tự thêm :scope vào trước. Ngoài ra, bạn có thể thêm bộ chọn &, từ CSS Nesting.

@scope (.card) {
    img {
       /* Selects img elements that are a child of .card */
    }
    :scope img {
        /* Also selects img elements that are a child of .card */
    }
    & img {
        /* Also selects img elements that are a child of .card */
    }
}

Giới hạn phạm vi có thể dùng lớp giả :scope để yêu cầu một mối quan hệ cụ thể với gốc phạm vi:

/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }

Giới hạn phạm vi cũng có thể tham chiếu đến các phần tử bên ngoài gốc phạm vi bằng cách sử dụng :scope. Ví dụ:

/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }

Bản thân các quy tắc về kiểu có phạm vi không thể thoát khỏi cây con. Các lựa chọn như :scope + p là không hợp lệ vì lựa chọn đó cố gắng chọn những phần tử không nằm trong phạm vi.

@scope và mức độ cụ thể

Các bộ chọn mà bạn sử dụng trong phần mở đầu cho @scope không ảnh hưởng đến độ cụ thể của các bộ chọn có trong đó. Trong ví dụ của chúng ta, độ đặc hiệu của bộ chọn img vẫn là (0,0,1).

@scope (#sidebar) {
  img { /* Specificity = (0,0,1) */
    ...
  }
}

Độ đặc hiệu của :scope là độ đặc hiệu của một giả lớp thông thường, cụ thể là (0,1,0).

@scope (#sidebar) {
  :scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
    ...
  }
}

Trong ví dụ sau, nội bộ, & được viết lại thành bộ chọn dùng cho gốc phạm vi, được bao bọc bên trong bộ chọn :is(). Cuối cùng, trình duyệt sẽ dùng :is(#sidebar, .card) img làm bộ chọn để thực hiện việc so khớp. Quá trình này được gọi là desugaring (loại bỏ cú pháp đường).

@scope (#sidebar, .card) {
    & img { /* desugars to `:is(#sidebar, .card) img` */
        ...
    }
}

& được huỷ đường bằng :is(), nên độ đặc hiệu của & được tính theo các quy tắc về độ đặc hiệu của :is(): độ đặc hiệu của & là độ đặc hiệu của đối số cụ thể nhất.

Áp dụng cho ví dụ này, độ đặc hiệu của :is(#sidebar, .card) là độ đặc hiệu của đối số cụ thể nhất, cụ thể là #sidebar và do đó trở thành (1,0,0). Kết hợp điều đó với độ cụ thể của img – tức là (0,0,1) – và bạn sẽ có (1,0,1) làm độ cụ thể cho toàn bộ bộ chọn phức tạp.

@scope (#sidebar, .card) {
  & img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
    ...
  }
}

Sự khác biệt giữa :scope& trong @scope

Ngoài sự khác biệt về cách tính độ cụ thể, một điểm khác biệt khác giữa :scope&:scope biểu thị gốc phạm vi trùng khớp, trong khi & biểu thị bộ chọn dùng để so khớp gốc phạm vi.

Do đó, bạn có thể sử dụng & nhiều lần. Điều này trái ngược với :scope mà bạn chỉ có thể sử dụng một lần, vì bạn không thể so khớp một gốc phạm vi bên trong một gốc phạm vi.

@scope (.card) {
  & & { /* Selects a `.card` in the matched root .card */
  }
  :scope :scope { /* ❌ Does not work */
    
  }
}

Phạm vi không có phần mở đầu

Khi viết kiểu nội tuyến bằng phần tử <style>, bạn có thể đặt phạm vi cho các quy tắc kiểu đối với phần tử mẹ bao quanh của phần tử <style> bằng cách không chỉ định bất kỳ gốc đặt phạm vi nào. Bạn thực hiện việc này bằng cách bỏ qua phần mở đầu của @scope.

<div class="card">
  <div class="card__header">
    <style>
      @scope {
        img {
          border-color: green;
        }
      }
    </style>
    <h1>Card Title</h1>
    <img src="…" height="32" class="hero">
  </div>
  <div class="card__content">
    <p><img src="…" height="32"></p>
  </div>
</div>

Trong ví dụ trên, các quy tắc có phạm vi chỉ nhắm đến những phần tử bên trong div có tên lớp là card__header, vì div đó là phần tử mẹ của phần tử <style>.

@scope trong tầng

Trong CSS Cascade, @scope cũng thêm một tiêu chí mới: phạm vi lân cận. Bước này diễn ra sau độ đặc hiệu nhưng trước thứ tự xuất hiện.

Hình ảnh trực quan về CSS Cascade.

Theo quy cách:

Khi so sánh các khai báo xuất hiện trong các quy tắc kiểu có gốc phạm vi khác nhau, thì khai báo có ít bước nhảy thế hệ hoặc phần tử anh chị em nhất giữa gốc phạm vi và đối tượng quy tắc kiểu có phạm vi sẽ thắng.

Bước mới này sẽ hữu ích khi bạn lồng một số biến thể của một thành phần. Hãy xem ví dụ này, ví dụ này chưa sử dụng @scope:

<style>
    .light { background: #ccc; }
    .dark  { background: #333; }
    .light a { color: black; }
    .dark a { color: white; }
</style>
<div class="light">
    <p><a href="#">What color am I?</a></p>
    <div class="dark">
        <p><a href="#">What about me?</a></p>
        <div class="light">
            <p><a href="#">Am I the same as the first?</a></p>
        </div>
    </div>
</div>

Khi xem đoạn đánh dấu nhỏ đó, đường liên kết thứ ba sẽ là white thay vì black, mặc dù đó là phần tử con của div có lớp .light được áp dụng cho phần tử đó. Điều này là do tiêu chí về thứ tự xuất hiện mà tầng sử dụng ở đây để xác định thành phần chiến thắng. Thấy rằng .dark a được khai báo sau cùng, nên nó sẽ giành chiến thắng từ quy tắc .light a

Với tiêu chí về phạm vi theo mức tương cận, vấn đề này hiện đã được giải quyết:

@scope (.light) {
    :scope { background: #ccc; }
    a { color: black;}
}

@scope (.dark) {
    :scope { background: #333; }
    a { color: white; }
}

Vì cả hai bộ chọn a có phạm vi đều có độ đặc hiệu như nhau, nên tiêu chí về khoảng cách trong phạm vi sẽ có hiệu lực. Nó cân nhắc cả hai bộ chọn theo khoảng cách đến gốc phạm vi của chúng. Đối với phần tử a thứ ba đó, chỉ có một bước nhảy đến gốc phạm vi .light nhưng có hai bước nhảy đến gốc phạm vi .dark. Do đó, bộ chọn a trong .light sẽ thắng.

Cách ly bộ chọn, không cách ly kiểu

Lưu ý rằng @scope giới hạn phạm vi tiếp cận của bộ chọn. Nó không cung cấp tính năng cách ly kiểu. Các thuộc tính kế thừa xuống các thành phần con vẫn kế thừa, vượt quá giới hạn dưới của @scope. Một trong những thuộc tính như vậy là thuộc tính color. Khi khai báo một thành phần bên trong phạm vi donut, color vẫn kế thừa xuống các thành phần con bên trong lỗ của donut.

@scope (.card) to (.card__content) {
  :scope {
    color: hotpink;
  }
}

Trong ví dụ này, phần tử .card__content và các phần tử con của phần tử này có màu hotpink vì chúng kế thừa giá trị từ .card.