Batasi jangkauan pemilih Anda dengan @scope CSS sesuai aturan

Pelajari cara menggunakan @scope untuk memilih elemen hanya dalam subpohon terbatas DOM Anda.

Dukungan Browser

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

Sumber

Seni penulisan yang cermat dalam menulis pemilih CSS

Saat menulis pemilih, Anda mungkin mendapati diri Anda terbelah di antara dua dunia. Di satu sisi Anda ingin lebih spesifik tentang elemen mana yang Anda pilih. Di sisi lain, Anda ingin pemilih Anda tetap mudah diganti dan tidak terkait erat dengan struktur DOM.

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

  • Pemilih ini memiliki kekhususan yang cukup tinggi dari (0,3,1) yang membuatnya sulit untuk diganti seiring pertumbuhan kode Anda.
  • Dengan mengandalkan combinator turunan langsung, kombinator ini dikaitkan secara erat dengan struktur DOM. Jika markup suatu saat berubah, Anda juga harus mengubah CSS Anda.

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 merupakan tantangan. Selama bertahun-tahun, beberapa developer telah menemukan solusi dan solusi untuk membantu Anda dalam situasi seperti ini. Contoh:

  • Metodologi seperti BEM mendikte bahwa Anda memberi elemen tersebut class card__img card__img--hero untuk menjaga kekhususannya tetap rendah sekaligus memungkinkan Anda untuk spesifik dalam apa yang dipilih.
  • 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 visual langsung di 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? Di sinilah @scope berperan, menawarkan cara untuk memilih elemen hanya dalam subpohon 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 ada – dinamai aturan gaya cakupan – hanya dapat memilih dari subhierarki terbatas DOM tersebut.

Misalnya, untuk menargetkan hanya elemen <img> di komponen .card, Anda harus menetapkan .card sebagai root pencakupan dari @scope yang aturan.

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

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

Untuk mencegah pemilihan elemen <img> di dalam area konten kartu (.card__content), Anda dapat membuat pemilih img lebih spesifik. Cara lain untuk melakukannya adalah dengan menggunakan fakta bahwa @scope pada aturan 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 pencakupan ini–dengan batas atas dan bawah–sering disebut sebagai cakupan donat

Pemilih :scope

Secara default, semua aturan gaya cakupan 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 menjelaskannya secara eksplisit dengan menambahkan sendiri :scope ke awal. 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 meng-escape subhierarki. Pilihan seperti :scope + p tidak valid karena tindakan tersebut mencoba memilih elemen yang tidak berada dalam cakupan.

@scope dan kekhususan

Pemilih yang Anda gunakan di awal untuk @scope tidak memengaruhi kekhususan pemilih yang ada. Pada contoh di bawah, kekhususan pemilih img masih adalah (0,0,1).

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

Kekhususan :scope adalah class semu 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(), kekhususan & dihitung dengan mengikuti aturan kekhususan :is(): kekhususan & adalah kekhususannya dari argumennya yang paling spesifik.

Diterapkan ke contoh ini, kekhususan :is(#sidebar, .card) adalah argumen 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 pencakupan.

Karena itu, Anda dapat menggunakan & beberapa kali. Hal ini berbeda dengan :scope yang hanya dapat Anda gunakan sekali, karena Anda tidak dapat mencocokkan root cakupan di dalam root pencakupan.

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

Cakupan tanpa awalan

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 melakukannya dengan menghilangkan awalan @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>.

@cakupan di air terjun

Di dalam Cascade CSS, @scope juga menambahkan kriteria baru: kedekatan pencakupan. Langkah tersebut dilakukan setelah kekhususan, tetapi sebelum urutan penampilan.

Visualisasi CSS Cascade.

Sesuai per spesifikasi:

Saat membandingkan deklarasi yang muncul di aturan gaya dengan akar cakupan yang berbeda, maka deklarasi dengan hop generasi atau elemen seinduk paling sedikit antara akar cakupan dan subjek aturan gaya cakupan akan menang.

Langkah baru ini berguna saat membuat beberapa variasi komponen bertingkat. Ambil 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 dan bukan black, meskipun link tersebut adalah turunan dari div dengan class .light yang diterapkan ke link tersebut. Ini disebabkan oleh urutan kriteria tampilan yang digunakan jenjang ini untuk menentukan pemenang. Ia melihat bahwa .dark a dideklarasikan terakhir, sehingga akan menang dari aturan .light a

Dengan kriteria kedekatan pencakupan, hal ini sekarang dapat diselesaikan:

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

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

Karena kedua pemilih a cakupan memiliki kekhususan yang sama, kriteria kedekatan pencakupan mulai diterapkan. Menimbang kedua pemilih berdasarkan jaraknya dengan root cakupannya. Untuk elemen a ketiga tersebut, nilai ini hanya memiliki satu hop ke root cakupan .light, tetapi dua hop ke root cakupan .dark. Oleh karena itu, pemilih a di .light akan menang.

Catatan penutup: Isolasi pemilih, bukan isolasi gaya

Satu catatan penting yang perlu diperhatikan adalah @scope membatasi jangkauan pemilih, dan tidak menawarkan isolasi gaya. Properti yang diturunkan ke turunan akan tetap mewarisinya, di luar batas bawah @scope. Salah satu properti tersebut adalah properti color. Saat mendeklarasikan bahwa objek tersebut berada di dalam cakupan donat, color akan tetap mewarisi turunan di dalam lubang donat tersebut.

@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)