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ọn các phần tử chỉ trong một cây con giới hạn của DOM.

Hỗ trợ trình duyệt

  • 118
  • 118
  • x
  • x

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

Khi viết bộ chọn, bạn có thể thấy mình bị chia nhỏ giữa hai thế giới. Một mặt, bạn muốn khá cụ thể về những phần tử 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 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 khá cụ thể đối với phần tử, 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 điểm khá cao là (0,3,1), khiến bạn khó ghi đè được khi mã phát triển.
  • Bằng cách dựa vào trình kết hợp con trực tiếp, trình kết hợp 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 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ì thao tá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 vấ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 chỉ ra rằng bạn cung cấp cho phần tử đó một lớp card__img card__img--hero để duy trì tính đặc hiệu thấp, trong khi vẫn cho phép bạn chỉ định những gì mình chọn.
  • Các giải pháp dựa trên JavaScript như CSS có phạm vi hoặc Thành phần được tạo kiểu sẽ ghi 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 của bạn để ngăn các bộ chọn này 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 xoá hoàn toàn các bộ chọn và yêu cầu bạn đặt trình kích hoạt định kiểu trực tiếp vào chính mã đánh dấu đó.

Nếu bạn không cần những gợi ý đó thì sao? Điều gì sẽ xảy ra nếu CSS cung cấp cho bạn một cách hoàn toàn cụ thể về những phần tử bạn chọn mà không yêu cầu bạn phải viết bộ chọn có tính đặc hiệu cao hoặc những bộ chọn được liên kết chặt chẽ với DOM của bạn? Đó là lúc @scope phát huy tác dụng, cung cấp cho bạn cách chọn các phần tử chỉ trong 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 những bộ chọn. Bạn thực hiện việc này bằng cách đặt gốc phạm vi nhằm xác định ranh giới trên của cây con bạn muốn nhắm mục tiêu. Với tập hợp gốc phạm vi, các quy tắc kiểu chứa – có tên là quy tắc kiểu theo 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 đặ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 theo phạm vi img { … } chỉ có thể chọn các phần tử <img> thuộc phạm vi của phần tử .card trùng khớp một cách hiệu quả.

Để các phần tử <img> trong vùng nội dung của thẻ (.card__content) không được chọn, bạn có thể chỉnh sửa bộ chọn img cụ thể hơn. Một cách khác để làm điều này là sử dụng thực tế là @scope có quy tắc 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 theo 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 đối tượng cấp trên. Loại phạm vi này (với ranh giới trên và giới hạn 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ả cá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 mục tiêu chính phần tử gốc trong 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 */
    }
}

Bộ chọn bên trong quy tắc kiểu theo phạm vi sẽ ngầm được thêm vào trước :scope. Nếu muốn, bạn có thể giải thích rõ ràng bằng cách tự đặt trước :scope. Ngoài ra, bạn có thể thêm vào trước bộ chọn & trong 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ể sử dụng lớp giả lập :scope để yêu cầu 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 của chúng 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 bản thân 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 là không hợp lệ vì cố gắng chọn các phần tử không nằm trong phạm vi.

@scope và tính cụ thể

Những bộ chọn mà bạn sử dụng ở phần mở đầu cho @scope không ảnh hưởng đến tính đặc hiệu của các bộ chọn có trong đó. Trong ví dụ bên dưới, đặc điểm 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, trong nội bộ, & được viết lại thành bộ chọn dùng cho phạm vi gốc, đượ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 để thực hiện so khớp. Quá trình này 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 tính đặc hiệu của & được tính theo các quy tắc về tính đặc hiệu của :is(): tính đặc hiệu của & là đối số cụ thể nhất.

Áp dụng cho ví dụ này, đặc điểm cụ thể của :is(#sidebar, .card) là đối số cụ thể nhất, cụ thể là #sidebar, và do đó trở thành (1,0,0). Hãy kết hợp dữ liệu đó với tính đặc hiệu của img – là (0,0,1) – rồi bạn sẽ có (1,0,1) làm đặc trưng 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ể, :scope& còn có một điểm khác biệt nữa là :scope đại diện cho gốc phạm vi đã 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 trong một gốc so khớp.

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

Phạm vi không có cảnh mở đầu

Khi viết kiểu cùng dòng với phần tử <style>, bạn có thể đặt phạm vi các quy tắc kiểu ở 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 xác định 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 đến các phần tử bên trong div có tên lớp card__header, vì div đó là phần tử mẹ của phần tử <style>.

@scope trên thá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 đứng sau tính cụ thể nhưng trước thứ tự xuất hiện.

Hình ảnh hoá 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 quy tắc kiểu có gốc phạm vi khác nhau, thì nội dung khai báo có bước nhảy phần tử cùng cấp hoặc thế hệ ít nhất giữa gốc phạm vi và chủ thể 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: hàm 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 phầ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. Điều này là do thứ tự của tiêu chí giao diện mà tầng sử dụng ở đây để xác định quảng cáo chiến thắng. Thiết bị này thấy rằng .dark a được khai báo sau cùng, nên sẽ thắng theo quy tắc .light a

Với tiêu chí xác định phạm vi độ gầ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; }
}

Bởi vì cả hai bộ chọn a theo phạm vi đều có cùng đặc điểm, nên tiêu chí xác định phạm vi độ gần sẽ bắt đầu hoạt động. Công cụ này cân nhắc cả hai bộ chọn bằng khoảng cách gần với gốc phạm vi của chúng. Đối với phần tử a thứ ba đó, đó chỉ là một hop đến gốc phạm vi .light nhưng là 2 hop đến gốc .dark. Do đó, bộ chọn a trong .light sẽ giành chiến thắng.

Lưu ý kết thúc: 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, không cung cấp tính năng tách biệt kiểu. Các thuộc tính kế thừa con cháu 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 như vậy là thuộc tính color. Khi khai báo một nội dung bên trong phạm vi biểu đồ vòng, color sẽ vẫn kế thừa cho các thành phần con bên trong lỗ của biểu tượng vòng tròn.

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

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