Menata Gaya Komponen Web

Shadow DOM adalah batas gaya: aturan halaman tidak menjangkau ke dalam dan aturan komponen tidak bocor ke luar, kecuali properti yang diwariskan (termasuk properti kustom). Lakukan dengan sengaja melalui saluran di bawah ini.

:host, :host(), :host-context()

/* The component's own host element, styled from inside its shadow tree. */
:host { display: block; }

/* Only when the host matches a selector (e.g. a reflected state attribute). */
:host([disabled]) { opacity: 0.5; pointer-events: none; }

/* Based on an ancestor in the Light DOM; useful for theming. */
:host-context([theme="dark"]) { background: #111; color: #eee; }

Hindari :host-context(). Pemilih ini menjangkau di luar batas untuk menguji ancestor, tetapi Firefox dan Safari menolak untuk menerapkannya dan CSSWG cenderung tidak mengembangkannya lebih lanjut — anggap pemilih ini sebagai jalan buntu khusus Chromium, bukan opsi yang berorientasi ke masa depan. Tema melalui properti kustom CSS yang diwarisi (di bawah): konsumen menetapkan properti pada ancestor dan komponen membacanya di seluruh batas, yang berfungsi di mana saja dan membuat kedua sisi tetap terpisah.

Properti yang diwariskan dan properti kustom melintasi batas

Properti CSS yang diwarisi (color, font-family, line-height, dan properti kustom CSS) akan melewati batas shadow. Hal ini menjadikan properti kustom sebagai API tema utama: ekspos properti tersebut dengan penggantian yang sesuai.

:host {
  /* Consumers override --card-bg from outside; the fallback keeps it usable. */
  background: var(--card-bg, white);
  color: var(--card-fg, black);
}

Properti kustom yang ditentukan di :host hanya dapat diganti oleh aturan yang menargetkan elemen host secara khusus. Agar properti ini dapat diwarisi dan diganti dari mana saja di hierarki, biarkan properti publik tidak ditetapkan di :host dan andalkan penggantian di setiap var(); jangan tetapkan properti publik di :host.

Agar penggantian dideklarasikan sekali, bukan diulang di setiap var(), tentukan properti "shadow" pribadi di :host yang membaca properti publik, dan referensikan properti pribadi secara internal:

:host {
  --_card-bg: var(--card-bg, white);
  --_card-fg: var(--card-fg, black);
}
.card { background: var(--_card-bg); color: var(--_card-fg); }

Konsumen masih mengganti --card-bg dari luar; garis bawah di awal menandakan bahwa alias bersifat internal, dan penggantiannya hanya ada di satu tempat.

::slotted(): menata gaya DOM Ringan yang diproyeksikan

::slotted(selector) menargetkan node tingkat teratas yang ditetapkan ke slot. Aturan ini tidak dapat menjangkau turunannya, dan kalah dari aturan Light DOM (konsumen memiliki kontennya sendiri), jadi gunakan aturan ini untuk sentuhan ringan, bukan kontrol. ::slotted(...) !important dapat mengalahkan aturan konsumen, tetapi hal ini akan mengganti gaya yang berhak dimiliki konsumen — simpan untuk perbaikan struktural, bukan penggantian estetika.

/* Matches a slotted <p>, but NOT a <p> nested inside a slotted <div>. */
::slotted(p) { margin: 0; }

::part() dan exportparts: mengekspos internal untuk pembuatan tema

Tandai elemen internal dengan part="name" agar konsumen dapat menata gayanya dengan properti apa pun, tanpa mempelajari struktur internal Anda.

<!-- inside the shadow template -->
<button part="submit">Submit</button>
/* consumer stylesheet */
my-card::part(submit) { background: navy; color: white; }

Ekspos beberapa bagian tingkat tinggi (tombol, input, label), bukan setiap <div>; permukaan bagian yang luas akan menjadi API yang rapuh. Untuk mengekspos ulang bagian komponen bertingkat ke atas melalui komponen Anda sendiri, teruskan bagian tersebut dengan exportparts:

<inner-widget exportparts="submit: card-submit"></inner-widget>

Pilih di antara dua saluran dengan sengaja: properti kustom untuk kumpulan nilai yang dibatasi dan aman untuk sistem desain; bagian untuk penataan gaya elemen tertentu yang tidak dibatasi.

Meneruskan gaya ke elemen native yang digabungkan

Saat elemen kustom menggabungkan kontrol native (<my-button> di sekitar <button> yang sebenarnya), ada tiga hal yang sering membuat orang bingung:

  • Kontrol formulir tidak mewarisi tipografi. color dan properti kustom melintasi batas, tetapi <button>, <input>, dan <select> mengambil font dan elemen terkait dari stylesheet UA, yang tidak diwarisi. Oleh karena itu, kontrol yang digabungkan akan dirender dalam font platform, bukan font halaman, hingga Anda menetapkan font: inherit (dan biasanya color: inherit) di dalamnya di dalam hierarki bayangan. Ini adalah satu-satunya kesalahan yang paling umum.
  • Gaya :host tidak menjangkau elemen dalam. Menyetel background/border/padding pada :host akan menata gaya kotak host, bukan kontrol di dalamnya. Teruskan dengan sengaja: ekspos kontrol sebagai ::part untuk gaya visual terbuka, atau teruskan kumpulan nilai tetap melalui properti kustom — aturan :host tidak di-cascade ke elemen yang digabungkan.
  • display: contents + all: inherit adalah kompromi yang nyata, bukan jalan pintas. Menciutkan host dengan display: contents dan mereset kontrol dalam dengan all: inherit saat ini merupakan satu-satunya cara umum untuk meneruskan gaya arbitrer ke elemen native yang digabungkan tanpa konsumen mempelajari bagian per komponen atau properti kustom — dan menghapus kotak host itu sendiri biasanya merupakan tujuan dari pola tersebut (komposisi mengompensasi pewarisan bawaan yang tidak ada). Dua peringatan nyata: all: inherit menghapus default UA, jadi cakupannya ke properti tertentu atau gunakan revert saat Anda ingin default kembali; dan pemblokir konkret — getBoundingClientRect() dan getClientRects() menampilkan nol untuk elemen display: contents, sehingga pemosisian anchor terhadap host rusak. Gunakan ::part atau properti kustom jika kompromi tersebut tidak sesuai.

:state(): menata gaya status komponen itu sendiri

Elemen kustom dapat mengekspos status boolean internal melalui ElementInternals melalui this.#internals.states.add('busy') dan .delete('busy') (lihat Elemen Kustom untuk penggunaan siklus proses), dan status tersebut menjadi saluran gaya di seluruh batas — seperti ::part dan properti kustom, tetapi untuk status, bukan struktur atau nilai. Cocokkan elemen tersebut di dalam shadow tree dengan :host(:state(name)), dan dari stylesheet konsumen dengan tag-name:state(name):

/* Inside the component's shadow tree. */
:host(:state(busy)) button { opacity: 0.6; pointer-events: none; }

/* From the consumer's stylesheet — a read-only public hook. */
favorite-button:state(favorited) { filter: drop-shadow(0 0 4px crimson); }

Tidak seperti atribut yang direfleksikan, status kustom bersifat hanya baca untuk pihak luar: konsumen dapat menata gayanya, tetapi tidak dapat menetapkan atau memalsukannya, sehingga komponen tetap menjadi satu-satunya penulis. Gunakan atribut ini daripada atribut data-* setiap kali status dimiliki oleh komponen dan hanya perlu diberi gaya.

@layer dan kueri penampung di dalam komponen

  • Lapisan bertingkat (@layer) berfungsi di dalam shadow root dan dicakup di dalamnya; nama lapisan di shadow tree tidak bergantung pada lapisan dengan nama yang sama di halaman. Gunakan untuk mengurutkan gaya komponen Anda sendiri (misalnya, @layer base, theme;) sehingga penggantian konsumen melalui properti kustom dan ::part masih menang seperti yang diperkirakan.
  • Kueri penampung adalah alat yang tepat untuk komponen responsif: komponen harus merespons ruang yang diberikan, bukan area pandang. Deklarasikan host atau wrapper sebagai penampung (memberi :host kotak tata letak seperti display: block, karena elemen kustom ditetapkan secara default ke display: inline dan pembatasan ukuran tidak berlaku untuk elemen inline atau contents) dan buat kueri, sehingga komponen yang sama beradaptasi di sidebar atau wilayah lebar penuh.
:host {
  display: block; /* Required: size containment does not apply to default display: inline */
  container-type: inline-size;
}

@container (min-width: 30rem) {
  .card { grid-template-columns: 1fr 2fr; }
}

Strategi penggantian

::part, lapisan cascade, dan kueri penampung tersedia secara luas; tema terutama melalui properti kustom yang diwariskan, yang berfungsi bahkan di tempat yang tidak mendukungnya. Seperti yang disebutkan di atas, jangan mengandalkan :host-context() — ini hanya berlaku untuk Chromium dan bukan opsi yang berorientasi ke masa depan.