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.

Hỗ trợ trình duyệt

  • Chrome: 118.
  • Edge: 118.
  • Firefox: phía sau một cờ.
  • Safari: 17.4.

Nguồn

Nghệ thuật tinh tế của việc viết bộ chọn CSS

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 cần phải khá cụ thể về những phần tử mà bạn chọn. Mặt khác, bạn muốn bộ chọn của mình vẫn dễ dàng ghi đè và không được ghép nối chặt chẽ với cấu trúc DOM.

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

  • Bộ chọn này có độ cụ thể khá cao của (0,3,1), khiến bạn khó có thể ghi đè khi mã của mình phát triển.
  • Bằng cách dựa vào toán tử kết hợp con trực tiếp, thành phần này được ghép nối 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 nên chỉ viết img làm bộ chọn cho phần tử đó, vì điều đó sẽ chọn tất cả phần tử hình ảnh trên trang.

Việc tìm ra sự cân bằng phù hợp trong trường hợp này thường là một thách thức. 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 giải quyết để giúp bạn trong những tình huống như vậy. Ví dụ:

  • Các phương pháp như BEM yêu cầu bạn cung cấp cho phần tử đó một lớp card__img card__img--hero để giữ mức độ cụ thể ở mức thấp trong khi vẫn cho phép bạn chọn cụ thể.
  • Các giải pháp dựa trên JavaScript như CSS có giới hạn hoặc Thành phần được tạo kiểu sẽ viết lại tất cả bộ chọ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 các 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 trình kích hoạt kiểu trực tiếp vào chính mã đánh dấu.

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

Giới thiệu @scope

Với @scope, bạn có thể giới hạn phạm vi tiếp cận của bộ chọn. Bạn thực hiện việc này bằng cách đặt gốc phạm vi để xác định ranh giới trên của cây con mà bạn muốn nhắm đến. Với một tập hợp gốc 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 trong cây con giới hạn đó 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 at-rule @scope.

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

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

Để ngăn các phần tử <img> bên trong vùng nội dung của thẻ (.card__content) bị 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ế là quy tắc tại @scope cũng chấp nhận 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 trong 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 cấp trê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 bánh rán

Bộ chọn :scope

Theo mặc định, tất cả quy tắc kiểu trong 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 trong 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 vào đầu một cách ngầm ẩn. 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 đầu. Ngoài ra, bạn có thể thêm bộ chọn & vào đầu, từ phần Lồng ghép CSS.

@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ể sử 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 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) { ... }

Xin lưu ý rằng các quy tắc kiểu trong phạm vi không thể thoát khỏi cây con. Các lựa chọn như :scope + p không hợp lệ vì lựa chọn đó cố gắng chọn các 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 tính cụ thể của các bộ chọn được chứa. Trong ví dụ bên dưới, tính cụ thể của bộ chọn img vẫn là (0,0,1).

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

Đặc điểm của :scope là của một lớp giả 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 gói bên trong bộ chọn :is(). Cuối cùng, trình duyệt sẽ sử dụng :is(#sidebar, .card) img làm bộ chọn để so khớp. Quá trình này được gọi là đơn giản hoá.

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

& được đơn giản hoá bằng :is(), nên mức độ cụ thể của & được tính theo các quy tắc về mức độ cụ thể của :is(): mức độ cụ thể của & là mức độ cụ thể của đối số cụ thể nhất.

Áp dụng cho ví dụ này, tính cụ thể của :is(#sidebar, .card) là tính cụ thể của đối số cụ thể nhất, cụ thể là #sidebar, do đó trở thành (1,0,0). Kết hợp điều đó với tính cụ thể của img – là (0,0,1) – và bạn sẽ có (1,0,1) làm tính 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& bên 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 đại diện cho gốc phạm vi được so khớp, trong khi & đại diện cho 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ó Prelude

Khi viết kiểu cùng dòng bằng phần tử <style>, bạn có thể đặt phạm vi cho các quy tắc kiểu cho phần tử mẹ bao bọc của phần tử <style> bằng cách không chỉ định bất kỳ phần tử gốc nào trong phạm vi. 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 trong phạm vi chỉ nhắm đến các 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 thác nước

Bên trong CSS Cascade (Loạt CSS), @scope cũng thêm một tiêu chí mới: phạm vi gần. Bước này xuất hiện sau mức độ cụ thể nhưng trước thứ tự xuất hiện.

Hình ảnh trực quan của CSS Cascade.

Theo thông số kỹ thuật:

Khi so sánh các nội dung khai báo xuất hiện trong quy tắc kiểu với các gốc phạm vi khác nhau, thì nội dung khai báo có ít nhất số lượt chuyển đổi phần tử con hoặc phần tử đồng cấp giữa gốc phạm vi và chủ thể quy tắc kiểu trong phạm vi sẽ thắng.

Bước mới này rất hữu ích khi lồng ghép một số biến thể của một thành phần. Hãy xem ví dụ sau đâ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 một chút mã đánh dấu đó, đườ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í thứ tự xuất hiện mà thác nước sử dụng tại đây để xác định người chiến thắng. Trình biên dịch thấy rằng .dark a được khai báo sau cùng, vì vậy, .dark a sẽ thắng theo quy tắc .light a

Với tiêu chí khoảng cách trong phạm vi, 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 trong phạm vi đều có cùng độ cụ thể, nên tiêu chí khoảng cách trong phạm vi sẽ bắt đầu hoạt động. Phương thức này đánh giá 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ần một bước nhảy đến gốc phạm vi .light nhưng cần hai bước nhảy đến gốc phạm vi .dark. Do đó, bộ chọn a trong .light sẽ thắng.

Lưu ý cuối cùng: Phân tách bộ chọn, chứ không phải phân tách kiểu

Một lưu ý quan trọng cần lưu ý là @scope giới hạn phạm vi của bộ chọn, không cung cấp tính năng tách biệt kiểu. Các thuộc tính kế thừa xuống phần tử con sẽ vẫn kế thừa, ngoài giới hạn dưới của @scope. Một trong những thuộc tính đó là color. Khi khai báo một trong phạm vi donut, color vẫn sẽ kế thừa xuống các phần tử con bên trong lỗ của donut.

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

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

(Ảnh bìa của rustam burkhanov trên Unsplash)