Batasi jangkauan pemilih Anda dengan @scope CSS sesuai aturan

Pelajari cara menggunakan @scope untuk memilih elemen hanya dalam sub-pohon terbatas dari DOM Anda.

Dukungan Browser

  • Chrome: 118.
  • Edge: 118.
  • Firefox: di balik flag.
  • Safari: 17.4.

Sumber

Seni halus menulis pemilih CSS

Saat menulis pemilih, Anda mungkin merasa ragu antara dua dunia. Di satu sisi, Anda ingin cukup spesifik tentang elemen yang dipilih. Di sisi lain, Anda ingin pemilih tetap mudah diganti dan tidak terikat erat dengan struktur DOM.

Misalnya, saat ingin memilih “gambar hero di area konten komponen kartu”–yang merupakan pemilihan elemen yang agak spesifik–Anda kemungkinan besar tidak ingin menulis pemilih seperti .card > .content > img.hero.

  • Pemilih ini memiliki spesifisitas (0,3,1) yang cukup tinggi sehingga sulit diganti seiring bertambahnya kode Anda.
  • Dengan mengandalkan penggabungan turunan langsung, penggabungan ini terikat erat dengan struktur DOM. Jika markup berubah, Anda juga harus mengubah CSS.

Namun, Anda juga tidak ingin hanya menulis img sebagai pemilih untuk elemen tersebut, karena tindakan tersebut akan memilih semua elemen gambar di seluruh halaman.

Menemukan keseimbangan yang tepat dalam hal ini sering kali cukup menantang. Selama bertahun-tahun, beberapa developer telah menemukan solusi dan solusi alternatif untuk membantu Anda dalam situasi seperti ini. Contoh:

  • Metodologi seperti BEM menentukan bahwa Anda harus memberi elemen tersebut class card__img card__img--hero agar spesifitasnya tetap rendah sekaligus memungkinkan Anda menentukan pilihan secara spesifik.
  • Solusi berbasis JavaScript seperti CSS Cakupan atau Komponen Bergaya menulis ulang semua pemilih dengan menambahkan string yang dibuat secara acak–seperti sc-596d7e0e-4–ke pemilih untuk mencegahnya menargetkan elemen di sisi lain halaman Anda.
  • Beberapa library bahkan menghapus pemilih sepenuhnya dan mengharuskan Anda menempatkan pemicu gaya secara langsung dalam markup itu sendiri.

Namun, bagaimana jika Anda tidak memerlukannya? Bagaimana jika CSS memberi Anda cara untuk menentukan elemen yang dipilih dengan cukup spesifik, tanpa mengharuskan Anda menulis pemilih dengan spesifitas tinggi atau pemilih yang terikat erat dengan DOM? Nah, di sinilah @scope berperan, menawarkan cara untuk memilih elemen hanya dalam sub-pohon DOM Anda.

Memperkenalkan @scope

Dengan @scope, Anda dapat membatasi jangkauan pemilih. Anda melakukannya dengan menetapkan root cakupan yang menentukan batas atas sub-pohon yang ingin Anda targetkan. Dengan menetapkan root cakupan, aturan gaya yang dimuat –yang disebut aturan gaya cakupan– hanya dapat memilih dari sub-pohon DOM yang terbatas tersebut.

Misalnya, untuk menargetkan hanya elemen <img> dalam komponen .card, Anda menetapkan .card sebagai root cakupan aturan at @scope.

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

Aturan gaya cakupan img { … } hanya dapat memilih elemen <img> yang tercakup dalam elemen .card yang cocok.

Untuk mencegah elemen <img> di dalam area konten kartu (.card__content) dipilih, Anda dapat membuat pemilih img lebih spesifik. Cara lain untuk melakukannya adalah dengan menggunakan fakta bahwa aturan at @scope juga menerima batas cakupan yang menentukan batas bawah.

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

Aturan gaya cakupan ini hanya menargetkan elemen <img> yang ditempatkan di antara elemen .card dan .card__content di hierarki ancestor. Jenis cakupan ini–dengan batas atas dan bawah–sering disebut sebagai cakupan donat

Pemilih :scope

Secara default, semua aturan gaya cakupan bersifat relatif terhadap root cakupan. Anda juga dapat menargetkan elemen root cakupan itu sendiri. Untuk melakukannya, gunakan pemilih :scope.

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

Pemilih di dalam aturan gaya cakupan secara implisit akan ditambahkan :scope. Jika mau, Anda dapat melakukannya secara eksplisit, dengan menambahkan :scope sendiri. Atau, Anda dapat menambahkan pemilih & di awal, dari Penetasan 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 */
    }
}

Batas cakupan dapat menggunakan pseudo-class :scope untuk mewajibkan hubungan tertentu ke root cakupan:

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

Batas cakupan juga dapat mereferensikan elemen di luar root cakupannya menggunakan :scope. Contoh:

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

Perhatikan bahwa aturan gaya cakupan itu sendiri tidak dapat keluar dari sub-pohon. Pilihan seperti :scope + p tidak valid karena mencoba memilih elemen yang tidak termasuk dalam cakupan.

@scope dan spesifitas

Pemilih yang Anda gunakan dalam pengantar untuk @scope tidak memengaruhi kekhususan pemilih yang dimuat. Pada contoh di bawah, spesifitas pemilih img masih (0,0,1).

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

Spesifikasi :scope adalah pseudo-class reguler, yaitu (0,1,0).

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

Dalam contoh berikut, secara internal, & akan ditulis ulang ke pemilih yang digunakan untuk root cakupan, yang digabungkan di dalam pemilih :is(). Pada akhirnya, browser akan menggunakan :is(#sidebar, .card) img sebagai pemilih untuk melakukan pencocokan. Proses ini dikenal sebagai desugaring.

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

Karena & di-desugar menggunakan :is(), spesifitas & dihitung dengan mengikuti aturan spesifitas :is(): spesifitas & adalah spesifitas argumennya yang paling spesifik.

Jika diterapkan pada contoh ini, kekhususan :is(#sidebar, .card) adalah argumennya yang paling spesifik, yaitu #sidebar, sehingga menjadi (1,0,0). Gabungkan dengan kekhususan img–yaitu (0,0,1)–dan Anda akan mendapatkan (1,0,1) sebagai kekhususan untuk seluruh pemilih kompleks.

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

Perbedaan antara :scope dan & di dalam @scope

Selain perbedaan dalam cara penghitungan kekhususan, perbedaan lain antara :scope dan & adalah :scope mewakili root cakupan yang cocok, sedangkan & mewakili pemilih yang digunakan untuk mencocokkan root cakupan.

Oleh karena itu, & dapat digunakan beberapa kali. Hal ini berbeda dengan :scope yang hanya dapat Anda gunakan satu kali, karena Anda tidak dapat mencocokkan root cakupan di dalam root cakupan.

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

Cakupan tanpa Prelude

Saat menulis gaya inline dengan elemen <style>, Anda dapat menentukan cakupan aturan gaya ke elemen induk yang mengapit elemen <style> dengan tidak menentukan root cakupan apa pun. Anda melakukannya dengan menghapus pengantar @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>

Pada contoh di atas, aturan cakupan hanya menargetkan elemen di dalam div dengan nama class card__header, karena div tersebut adalah elemen induk elemen <style>.

@scope dalam cascade

Di dalam Cascade CSS, @scope juga menambahkan kriteria baru: proximity cakupan. Langkah ini dilakukan setelah kekhususan, tetapi sebelum urutan kemunculan.

Visualisasi CSS Cascade.

Sesuai dengan spesifikasi:

Saat membandingkan deklarasi yang muncul dalam aturan gaya dengan root cakupan yang berbeda, deklarasi dengan lompatan elemen generasi atau elemen yang paling sedikit antara root cakupan dan subjek aturan gaya yang dicakup akan menang.

Langkah baru ini berguna saat menyusun beberapa variasi komponen. Lihat contoh ini, yang belum menggunakan @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>

Saat melihat sedikit markup tersebut, link ketiga akan menjadi white, bukan black, meskipun merupakan turunan dari div dengan class .light yang diterapkan padanya. Hal ini disebabkan oleh kriteria urutan kemunculan yang digunakan cascade di sini untuk menentukan pemenang. Fungsi ini melihat bahwa .dark a dideklarasikan terakhir, sehingga akan menang dari aturan .light a

Dengan kriteria kedekatan cakupan, masalah ini kini telah teratasi:

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

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

Karena kedua pemilih a cakupan memiliki spesifitas yang sama, kriteria kedekatan cakupan akan diterapkan. Fungsi ini menimbang kedua pemilih berdasarkan kedekatan dengan root cakupannya. Untuk elemen a ketiga tersebut, hanya ada satu hop ke root cakupan .light, tetapi dua hop ke root .dark. Oleh karena itu, pemilih a di .light akan menang.

Catatan penutup: Isolasi pemilih, bukan isolasi gaya

Satu catatan penting yang perlu dibuat adalah @scope membatasi jangkauan pemilih, tetapi tidak menawarkan isolasi gaya. Properti yang diwariskan ke turunan akan tetap diwariskan, di luar batas bawah @scope. Salah satu properti tersebut adalah properti color. Saat mendeklarasikannya di dalam cakupan donat, color akan tetap mewarisi turunan di dalam lubang donat.

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

Pada contoh di atas, elemen .card__content dan turunannya memiliki warna hotpink karena mewarisi nilai dari .card.

(Foto sampul oleh rustam burkhanov di Unsplash)