Batasi jangkauan pemilih Anda dengan @scope CSS sesuai aturan

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

Dukungan Browser

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

Sumber

Seni penulisan yang cermat dalam menulis pemilih CSS

Saat menulis pemilih, Anda mungkin merasa ragu antara dua dunia. Di satu sisi Anda ingin lebih spesifik tentang elemen mana yang Anda pilih. 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 berkembangnya 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 hal tersebut akan memilih semua elemen gambar di seluruh halaman Anda.

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 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 Anda 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.

Tetapi bagaimana jika Anda tidak membutuhkannya? Bagaimana jika CSS memberi Anda cara untuk menjadi cukup spesifik tentang elemen mana yang Anda pilih, tanpa mengharuskan Anda menulis pemilih dengan kekhususan tinggi atau yang terkait erat dengan DOM Anda? 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 scoping root yang menentukan batas atas subhierarki yang ingin Anda targetkan. Dengan kumpulan root cakupan, aturan gaya yang terdapat – dinamai aturan gaya cakupan – hanya dapat memilih dari subpohon terbatas DOM 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 dalam 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 terbatas secara implisit akan menambahkan :scope. Jika mau, Anda dapat melakukannya secara eksplisit, dengan menambahkan :scope sendiri. Atau, Anda dapat menambahkan awalan pemilih &, dari Penyambungan 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 class semu :scope untuk mewajibkan hubungan khusus dengan 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, kekhususan pemilih img masih adalah (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, & 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). Kombinasikan dengan kekhususan img–yang adalah (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.

Karena itu, Anda dapat menggunakan & 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 mencakup elemen <style> dengan tidak menentukan root cakupan. Anda dapat 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 di air terjun

Di dalam Cascade CSS, @scope juga menambahkan kriteria baru: kedekatan pencakupan. 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)