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.
  • Cạnh: 118.
  • Firefox: phía sau một lá cờ.
  • Safari: 17.4.

Nguồn

Nghệ thuật tinh tế khi viết bộ chọn CSS

Khi viết bộ chọn, bạn có thể thấy mình bị chia rẽ giữa hai thế giới. Một mặt, bạn muốn biết rõ những yếu tố mình chọn. 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 được kết hợp 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ẻ", tức là một lựa chọn phần tử khá cụ thể, rất có thể bạn sẽ không muốn viết bộ chọn như .card > .content > img.hero.

  • Bộ chọn này có đặc điểm khá cao về (0,3,1), khiến bạn khó ghi đè khi mã của bạn phát triển.
  • Bằng cách dựa vào bộ kết hợp con trực tiếp, nó được kết hợp 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 của mình.

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ì việc này sẽ chọn tất cả các phần tử hình ảnh trên trang của bạn.

Tìm được sự cân bằng hợp lý trong giai đoạn này thường khá khó khă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 giải quyết để 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 cho biết bạn cung cấp cho phần tử đó một lớp card__img card__img--hero để duy trì tính cụ thể thấp trong khi vẫn cho phép bạn chọn nội dung 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ẽ ghi lại tất cả cá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 của bạn để ngăn chúng nhắm mục tiêu các phần tử ở phía bên kia của trang.
  • Một số thư viện thậm chí còn huỷ bỏ hoàn toàn bộ chọn và yêu cầu bạn đặt trực tiếp trình kích hoạt tạo kiểu vào chính mã đánh dấu.

Nhưng nếu bạn không có bất kỳ nhu cầu nào trong số đó? Điều gì sẽ xảy ra nếu CSS cung cấp cho bạn cách để vừa cụ thể về những phần tử bạn chọn mà không yêu cầu bạn viết các bộ chọn có tính đặc thù cao hoặc những bộ chọn được kết hợp chặt chẽ với DOM của bạn? Đó là khi @scope ra mắt, mang đến cho bạn một cách để chọn các phần tử chỉ 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 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, giúp xác định ranh giới trên của cây con mà bạn muốn nhắm mục tiêu. Với bộ gốc có phạm vi, các quy tắc kiểu được chứa (có tên là quy tắc kiểu trong phạm vi) chỉ có thể chọn từ 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 thiết lập .card làm gốc phạm vi của quy tắc at-@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 được so khớp.

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

Bộ chọn :scope

Theo mặc định, tất cả quy tắc kiểu trong phạm vi đều liên quan đến gốc phạm vi. Bạn cũng có thể nhắm mục tiêu chính phần tử gốc có phạm vi. Để thực hiện 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 */
    }
}

Các bộ chọn bên trong các quy tắc kiểu có giới hạn sẽ ngầm được thêm vào trước :scope. Nếu muốn, bạn có thể nêu rõ điều này bằng cách tự đặt tiền tố :scope. Ngoài ra, bạn có thể thêm bộ chọn & trong phần CSS Nesting (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 đế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) { ... }

Lưu ý rằng bản thân các quy tắc 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 không hợp lệ vì lựa chọn đó cố gắng chọn các phần tử không thuộc phạm vi.

@scope và độ cụ thể

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

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

Điểm đặc trưng của :scope là điểm đặc trưng 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, trong nội bộ, & được viết lại thành bộ chọn dùng cho thư mục 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ẽ 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 độ cụ thể của & được tính theo các quy tắc về tính cụ thể của :is(): tính cụ thể của & là đối số cụ thể nhất của nó.

Áp dụng cho ví dụ này, điểm cụ thể của :is(#sidebar, .card) là đối số cụ thể nhất của nó, cụ thể là #sidebar, và do đó trở thành (1,0,0). Kết hợp điều đó với tính cụ thể của img (chính là (0,0,1)) và cuối cùng bạn sẽ chọn (1,0,1) làm điể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& bên trong @scope

Bên cạnh 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 trùng khớp, trong khi & đại diện cho bộ chọn dùng để khớp với 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 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 mở đầu

Khi viết kiểu cùng dòng bằng phần tử <style>, bạn có thể thiết lập phạm vi của các quy tắc kiểu trong phần tử mẹ chứa phần tử <style> bằng cách không chỉ định bất kỳ gốc 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 theo phạm vi chỉ nhắm mục tiêu đế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, @scope cũng thêm một tiêu chí mới: phạm vi vùng lân cận. Bước này xuất hiện sau tính cụ thể nhưng trước thứ tự xuất hiện.

Hình ảnh của tầng CSS.

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

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

Bước mới này hữu ích khi lồng nhiều biến thể của một thành phần. Hãy xem ví dụ sau đây, ví dụ 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 phần đánh dấu nhỏ đó, đường liên kết thứ ba sẽ là white thay vì black, mặc dù đây là phần tử con của div có lớp .light được áp dụng. Điều này là do thứ tự tiêu chí xuất hiện mà tầng sử dụng ở đây để xác định quảng cáo chiến thắng. Nó thấy rằng .dark a được khai báo gần đây nhất nên sẽ chiến thắng theo quy tắc .light a

Với tiêu chí phạm vi lân 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 trong phạm vi đều có cùng đặc điểm, nên tiêu chí vùng lân cận sẽ hoạt động. Công cụ này cân nhắc cả hai bộ chọn khi gần đến gốc phạm vi của chúng. Đối với phần tử a thứ ba đó, phần tử này chỉ là một bước nhảy đến gốc phạm vi .light nhưng là hai bước đến .dark một. Do đó, bộ chọn a trong .light sẽ giành chiến thắng.

Ghi chú đóng: Tách biệt bộ chọn, không tách biệt kiểu

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

@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ử đó có màu hotpink vì chúng kế thừa giá trị của .card.

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