IlmHamroh
JavaScript Full-stack/13-qism. Brauzer API'lari, performans va xavfsizlik26/32-dars20 daqiqa
Mundarija (39)

CSS va Shadow DOM: :host, ::slotted(), ::part() bilan web komponentlarni stillash

Qisqacha: Shadow DOM sahifa CSS'ini ichkariga kiritmaydi, lekin to'liq yopiq emas — uning maxsus "eshiklari" bor. Ichkaridan: :host (host'ning o'zi), :host(.main) (holatiga qarab), ::slotted(…) (slotga tushgan kontent). Tashqaridan: meros bo'ladigan xususiyatlar (color, font), CSS o'zgaruvchilari (chegaradan o'tadi — mavzu uchun asosiy yo'l) va ::part(nom) (komponent part="nom" bilan ochgan elementlar). Bir stilni ko'p komponent bo'lishishi uchun — adoptedStyleSheets.

Bu darsda

  • Shadow chegarasidan nima o'tishi va nima o'tmasligini aniq ayta olasiz.
  • Komponent ichidan host'ni :host va :host() bilan, slotdagi kontentni ::slotted() bilan bezay olasiz.
  • Komponentni tashqaridan CSS o'zgaruvchilari va ::part() bilan sozlashga imkon bera olasiz.
  • Tashqi va ichki qoidalar to'qnashsa, qaysi biri yutishini (kaskaddagi "kontekst" bosqichi) tushuntira olasiz.
  • adoptedStyleSheets va :state() ni ishlata olasiz.

Oldin bilishingiz kerak: Web Components'ga kirish, Web Components, Kaskad, Meros va global kalit so'zlar, CSS o'zgaruvchilari 2.

1. Nega bu kerak?

O'tgan darsda <dish-card> ni yozdik. Uning stili Shadow DOM ichida — hamkor kafe sahifasining h3 qoidasi uni endi buzmaydi. Muammo hal bo'ldi.

Lekin yangi so'rov keldi. Hamkor kafe brendi to'q sariq. Ular aytdi: "Kartangiz yaxshi, faqat sarlavhasi bizning rangimizda bo'lsin, tugmasi esa bizning tugmalarimizdek dumaloq". Sardor hamkor sahifasiga dish-card h3 { color: orange; } yozib ko'rdi — hech narsa o'zgarmadi. Albatta: biz aynan shuni xohlagan edik, tashqi CSS ichkariga kirmasin.

Demak, ikki talab bir-biriga zid tuyuladi:

  1. Tashqi CSS komponentni tasodifan buzmasin.
  2. Tashqi sahifa komponentni ataylab sozlay olsin.

Shadow DOM ikkalasini ham beradi. U devor emas, eshiklari bor uy. Bu darsda shu eshiklarni birma-bir ochamiz: qaysilarini komponent muallifi ochadi va qaysilarini sahifa muallifi ishlatadi.

2. Chegaradan nima o'tadi

2.1 Jadval

Avvalgi ikki darsda chegaraning asosiy qoidasini ko'rdik. Endi to'liq ro'yxat. Hammasini Chrome 154 da tekshirdik:

Narsa Chegaradan o'tadimi?
Sahifadagi selektorlar (h3, .price, *) Yo'q — ichki elementlarni topmaydi
Meros bo'ladigan xususiyatlar (color, font-family, line-height) Ha — host'dan ichkariga meros
CSS o'zgaruvchilari (--color) Ha — ular ham meros bo'ladi
Host'ning o'ziga yozilgan qoida (dish-card { … }) Ha — host light DOM'da
::part() bilan ochilgan elementlar Ha — faqat ochilganlari

Bitta kutilmagan oqibat. Ko'p loyihada umumiy "reset" bor: * { box-sizing: border-box; }. U shadow ichiga kirmaydi. Chrome'da tekshirdik: sahifada shu qoida bor, lekin shadow ichidagi div content-box qolib ketdi. Komponent kerakli reset'ni o'z ichiga yozishi kerak.

2.2 CSS o'zgaruvchilari — mavzuning asosiy yo'li

CSS o'zgaruvchilari meros bo'ladi. Ichkarida var(--name) ishlatilsa, qiymat host'dan, undan yuqoridagi body dan, :root dan keladi. Shuning uchun komponent muallifi "sozlanadigan joylar"ni o'zgaruvchi qilib qo'yadi:

css
/* komponent ichida */
h3 { color: var(--dish-card-color, #2e7d32); }

var() ning ikkinchi qiymati — zaxira (CSS o'zgaruvchilari 2 darsidan): sahifa hech narsa bermasa, yashil.

Endi hamkor sahifasi bitta qator yozadi:

css
/* hamkor sahifasida */
:root { --dish-card-color: #d35400; }

Sinab ko'ring. Uch karta: birinchisiga hech narsa berilmagan, ikkinchisi butun sahifa mavzusini oladi, uchinchisiga alohida rang berilgan:

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  .partner { --dish-card-color: #d35400; }
  #special { --dish-card-color: #6a1b9a; }
</style>
<dish-card>Osh</dish-card>
<div class="partner">
  <dish-card>Manti</dish-card>
  <dish-card id="special">Lag'mon</dish-card>
</div>
<script>
  class DishCard extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: "open" }).innerHTML = `
        <style>
          :host { display: block; margin-bottom: 0.5rem; }
          h3 { margin: 0; color: var(--dish-card-color, #2e7d32); }
        </style>
        <h3><slot></slot></h3>`;
    }
  }
  customElements.define("dish-card", DishCard);

  for (const card of document.querySelectorAll("dish-card")) {
    const h3 = card.shadowRoot.querySelector("h3");
    console.log(card.textContent, getComputedStyle(h3).color);
  }
</script>

Konsolda:

text
Osh rgb(46, 125, 50)
Manti rgb(211, 84, 0)
Lag'mon rgb(106, 27, 154)

Nimaga qarang: komponent kodi bitta, ranglar uch xil. Osh — zaxira yashil. Manti — .partner dan meros olgan to'q sariq. Lag'mon — o'ziga yozilgan binafsha: yaqinroq ota yutadi, oddiy meros qoidasi.

Komponentning sozlanadigan o'zgaruvchilari — uning ommaviy API'si. Dizayn tizimlari ularni hujjatda alohida ro'yxat qilib yozadi ("CSS custom properties"). Nom oldiga komponent nomini qo'shish (--dish-card-…) to'qnashuvdan saqlaydi: sahifadagi boshqa --color komponentni tasodifan bo'yab qo'ymaydi.

Tekshirib ko'ring: Komponent ichida h3 { color: var(--color); } (zaxirasiz) yozilgan. Sahifa --color ni hech qayerda bermagan. Sarlavha qanday rangda bo'ladi?

Javob

Meros rangida — host'ning color i (masalan, body dagi qora). Zaxirasiz var() ga qiymat topilmasa, xususiyat "hisoblashda yaroqsiz" bo'ladi va color uchun meros qiymat olinadi (CSS o'zgaruvchilari 2 darsidagi qoida). Xato chiqmaydi, lekin dizayner kutgan rang ham chiqmaydi. Shuning uchun zaxira qiymat yozish — yaxshi odat.

3. :host — host'ni ichkaridan bezash

3.1 Asosiy shakl

Komponent ichidagi <style> host'ning o'zini oddiy selektor bilan topa olmaydi — host shadow daraxtda emas. Buning uchun maxsus pseudo-klass bor: :host.

css
:host {
  display: block;
  padding: 0.75rem 1rem;
  border: 1px solid #c8d6c8;
}

Muhim: custom element'ning standart display qiymati — inline, xuddi span kabi. Chrome'da tekshirdik: hech narsa yozilmagan host display: inline. Karta, panel kabi blok komponentlarda :host { display: block; } deyarli doim kerak. Aks holda padding va margin g'alati ishlaydi, width esa umuman ishlamaydi.

:host da yashirish uchun ham qoida qo'shish odat: :host([hidden]) { display: none; }. Sababi — display: block brauzerning [hidden] qoidasini bosib ketadi.

3.2 :host() — holatga qarab

Funksional shakl :host(selektor) — "host shu selektorga mos kelsa". Web Components darsidagi kartada ishlatgan edik:

css
:host([sold-out]) { opacity: 0.5; }      /* atribut bor bo'lsa */
:host(.main) { border-width: 3px; }   /* klass bor bo'lsa */
:host(:hover) { background: #f3f8f3; }  /* sichqoncha ustida */
:host(:focus-within) { outline: 2px solid #2e7d32; }

Qavs ichida faqat host'ning o'zi haqidagi oddiy (compound) selektor bo'ladi. :host(.menu .main) — host'ning otasi haqida so'rash — ishlamaydi.

3.3 :host-context() — ishlatmang

Ba'zi maqolalarda :host-context(.dark) ni uchratasiz: "host'ning ota-bobolari orasida .dark bo'lsa". U faqat Chromium brauzerlarida (Chrome, Edge) bor. Firefox va Safari uni qo'llamaydi — web-features 3.40 da :host-context() Baseline emas.

Uning o'rniga — CSS o'zgaruvchisi. Tungi rejimda sahifa --dish-card-bg ni o'zgartiradi, komponent shuni o'qiydi. Natija bir xil, hamma brauzerda ishlaydi.

4. Kaskad: ichkari va tashqari to'qnashsa

4.1 Sinov

Endi qiziq savol. Komponent ichida :host { border: 1px solid green; }. Sahifada esa dish-card { border: 3px solid orange; }. Ikkalasi bitta elementga — host'ga — tegadi. Qaysi biri yutadi?

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  dish-card { border: 3px solid #d35400; }
  dish-card { color: #d35400 !important; }
</style>
<dish-card>Osh</dish-card>
<script>
  class DishCard extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: "open" }).innerHTML = `
        <style>
          :host { display: block; padding: 0.5rem;
            border: 1px solid #2e7d32;
            color: #2e7d32 !important; }
        </style>
        <slot></slot>`;
    }
  }
  customElements.define("dish-card", DishCard);

  const host = document.querySelector("dish-card");
  const style = getComputedStyle(host);
  console.log("border:", style.borderTopWidth, style.borderTopColor);
  console.log("color:", style.color);
  console.log("display:", style.display);
</script>

Konsolda:

text
border: 3px rgb(211, 84, 0)
color: rgb(46, 125, 50)
display: block

Uch natija, uch xulosa:

  • Ramka — tashqariniki. Oddiy (!important siz) qoidalarda sahifa yutadi. Ichki :host — komponent muallifining "standart" taklifi. Sahifa uni bemalol o'zgartira oladi.
  • Rang — ichkariniki. !important qoidalarda tartib teskari: ichki !important tashqi !important dan kuchli. Komponent muallifi "bunga tegmang" deb qat'iy turib olishi mumkin.
  • display — ichkariniki. Sahifa display haqida hech narsa demadi — to'qnashuv yo'q.

4.2 Va'da: kaskadning "kontekst" bosqichi

Kaskad darsida "standartda yana ikkita kam uchraydigan bosqich bor, biri Shadow DOM darsida" degan edik. Mana u — kontekst (encapsulation context) bosqichi. U manba va !important bosqichidan keyin, inline stildan oldin turadi:

flowchart TD
  A["Bir xususiyatga<br/>bir nechta qiymat"] --> B["1. Manba va !important"]
  B -->|"teng"| K["Kontekst:<br/>tashqarimi, ichkarimi?"]
  K -->|"teng"| C["2. Inline stil"]
  C -->|"teng"| D["3. Kaskad qatlami"]
  D -->|"teng"| E["4. Specificity"]
  E -->|"teng"| F["5. Yozilish tartibi"]
  F --> G["G'olib qiymat"]

Nimaga qarang: kontekst specificity'dan oldin. Ya'ni ichki :host ning specificity'si qanchalik baland bo'lmasin, oddiy qoidalarda tashqi dish-card (bitta teg — eng past og'irlik) baribir yutadi. Bu ataylab qilingan: komponentdan foydalanuvchi sahifa egasi oxirgi so'zni aytishi kerak.

Qoida qisqacha: oddiy qoidada tashqi kontekst yutadi, !important da ichki kontekst yutadi. Bu !important ning umumiy mantiqiga mos: Kaskad darsida ko'rganimizdek, !important da manbalar tartibi ham teskari aylanadi.

Tekshirib ko'ring: Komponent ichida :host { margin: 1rem; }, sahifada esa body dish-card.main#hero { margin: 0; } (juda yuqori specificity). Qaysi margin ishlaydi? Agar sahifa qoidasi dish-card { margin: 0; } bo'lsa-chi?

Javob

Ikkala holatda ham margin: 0 — tashqi qoida. Specificity bu yerda ahamiyatsiz: kontekst bosqichi undan oldin hal qiladi va oddiy qoidalarda tashqi kontekst yutadi. Ichki qiymat faqat sahifa margin haqida umuman gapirmasa ishlaydi.

5. ::slotted() — slotga tushgan kontent

5.1 Asosiy shakl

template va slot darsida va'da bergan edik: slotga tushgan kontentni komponent ichidan bezash — ::slotted() pseudo-elementi bilan.

css
::slotted(p) { margin: 0; color: #555; }
::slotted([slot="price"]) { font-weight: 700; }
::slotted(*) { font-size: 0.95rem; }

Qavs ichiga oddiy (compound) selektor yoziladi: teg, klass, atribut va ularning birikmasi. Psevdo-klass qavs ichida bo'lishi mumkin — ::slotted(a:hover).

5.2 Uchta cheklov

Chrome 154 da birma-bir tekshirdik:

  1. Faqat yuqori darajadagi elementlar. ::slotted(p) — slotga bevosita tushgan p. Uning ichidagi span ni topib bo'lmaydi: ::slotted(p span) — yaroqsiz selektor, butun qoida tashlab yuboriladi. ::slotted(*) b ham yaroqsiz.
  2. Qavsdan keyin psevdo-klass yo'q. ::slotted(a):hover — yaroqsiz. To'g'risi — ::slotted(a:hover).
  3. Sahifa yutadi. Slotdagi kontent light DOM'da — u sahifaga tegishli. Sahifadagi span.note { color: purple } va ichki ::slotted(span) { color: teal } to'qnashsa — binafsha. Bu o'sha kontekst qoidasi: oddiy qoidalarda tashqi yutadi. Ichki qoida faqat sahifa gapirmagan xususiyatlarga ta'sir qiladi.

Bu cheklovlar mantiqli. Slotdagi kontent — sahifa muallifiniki. Komponent unga faqat "standart ko'rinish" taklif qiladi, ichiga chuqur kirib, egasidan kuchliroq bo'la olmaydi.

5.3 Matnni bezash — slot ning o'ziga

Slotga oddiy matn tushsa (teg ichida emas), ::slotted() uni topa olmaydi — matn element emas. Yechim: slot elementining o'ziga stil berish. slot ning display qiymati — contents (o'z qutisi yo'q), lekin meros bo'ladigan xususiyatlar undan slotdagi kontentga o'tadi. Chrome'da tekshirdik: slot { color: green; } slotdagi span ni yashil qildi.

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  span.note { color: #6a1b9a; }
</style>
<dish-card>
  <span slot="name">Osh</span>
  <span class="note">Juma kuni maxsus</span>
  Guruch, sabzi, go'sht.
</dish-card>
<script>
  class DishCard extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: "open" }).innerHTML = `
        <style>
          h3 { margin: 0; }
          ::slotted(span) { color: #00897b; font-style: italic; }
          slot:not([name]) { color: #555; }
        </style>
        <h3><slot name="name"></slot></h3>
        <p><slot></slot></p>`;
    }
  }
  customElements.define("dish-card", DishCard);

  const note = document.querySelector(".note");
  console.log("izoh rangi:", getComputedStyle(note).color);
  console.log("izoh shrifti:", getComputedStyle(note).fontStyle);
</script>

Konsolda:

text
izoh rangi: rgb(106, 27, 154)
izoh shrifti: italic

Nimaga qarang: "Juma kuni maxsus" — binafsha (sahifa rangi yutdi), lekin qiya (sahifa font-style haqida gapirmadi, ichki qoida ishladi). "Osh" — firuza va qiya: unga sahifa qoidasi tegmaydi. Oddiy matn "Guruch, sabzi, go'sht." — kulrang, slot:not([name]) dan meros oldi.

6. ::part() — tashqariga ochilgan elementlar

6.1 Komponent eshik ochadi

CSS o'zgaruvchilari bitta-bitta qiymatni beradi: rang, o'lcham. Hamkor esa tugmani butunlay qayta bezamoqchi: dumaloq, soyali, boshqa shrift. Har xususiyat uchun alohida o'zgaruvchi — noqulay.

Buning uchun komponent muallifi ichki elementga part atributini qo'yadi:

html
<!-- komponent ichida -->
<button part="button">Savatga</button>
<h3 part="title">Osh</h3>

part qiymati — o'zimiz tanlagan nom ("button" — tugma, "title" — sarlavha). U teg nomi emas: part="title" h3 da turibdi.

Sahifa esa uni ::part() pseudo-elementi bilan bezaydi:

css
/* sahifada */
dish-card::part(button) {
  border-radius: 999px;
  background: #d35400;
  color: white;
}
dish-card::part(button):hover { background: #a84300; }

part — komponentning ommaviy API'si, xuddi CSS o'zgaruvchilari kabi. Muallif qaysi elementlarni ochishni o'zi tanlaydi. Ochilmagan elementlar yopiq qoladi.

Bo'sh joyni to'ldiring: komponent ichida <button part="button"> bo'lsa, sahifa uni dish-card::(button) selektori bilan bezaydi.

6.2 Nima mumkin va nima mumkin emas

Chrome 154 da tekshirdik:

Selektor Holati
dish-card::part(button)
dish-card::part(button):hover, :focus-visible foydalanuvchi holatlari
dish-card::part(button)::before pseudo-element
dish-card::part(button) span ichiga kirish yo'q
dish-card::part(button):first-child tuzilma psevdo-klasslari yo'q

Mantiq bir xil: part — eshik, lekin faqat ochilgan elementning o'zigacha. Uning ichki tuzilmasi (span, tartib) yopiq qoladi. Shunda komponent muallifi ichki tuzilmani o'zgartirsa, sahifaning CSS'i buzilmaydi.

Bitta elementda bir nechta nom bo'lishi mumkin: part="button primary". ::part(button primary) — ikkala nom bor elementni topadi.

6.3 exportparts — ichma-ich komponentlar

<dish-card> ichida yana bir komponent bor deylik — <price-badge> ("narx belgisi", o'tgan darsning 1-mashqi), uning ichida part="value". Sahifa dish-card::part(value) yozsa — ishlamaydi. ::part() faqat bir qavat chuqurlikka ishlaydi.

Ichki komponentning part ini yuqoriga uzatish uchun — exportparts:

html
<!-- dish-card ichida -->
<price-badge exportparts="value: price-value"></price-badge>

Endi sahifa dish-card::part(price-value) yoza oladi. Ikki nuqta orqali qayta nomlash ixtiyoriy: exportparts="value" — nom o'zgarmasdan uzatiladi. Chrome'da tekshirdik: exportparts li ichki element tashqi ::part() qoidasini oldi, exportparts siz qo'shni nusxa esa olmadi.

Tekshirib ko'ring: Hamkor tugma ichidagi ikonkani (<button part="button"><svg>…</svg> Savatga</button>) yashirmoqchi. dish-card::part(button) svg { display: none; } ishlaydimi? Komponent muallifi nima qilishi kerak?

Javob

Ishlamaydi — ::part() dan keyin ichki elementga o'tib bo'lmaydi, selektor yaroqsiz. Muallif ikonkaga alohida part="icon" berishi kerak, keyin dish-card::part(icon) { display: none; }. Yoki ikonkani atribut bilan boshqaradi: :host([no-icon]) svg { display: none; }.

7. adoptedStyleSheets — bitta stil, ko'p komponent

7.1 Muammo

Hozirgacha har karta o'z <style> tegini oladi. 30 ta karta — 30 ta <style>, brauzer har birini alohida saqlaydi. Dizaynni o'zgartirish uchun esa har nusxaga tegish kerak.

7.2 Constructable stylesheets

Constructable stylesheet (JavaScript'da yasaladigan stil jadvali) — new CSSStyleSheet() bilan yaratiladigan stil obyekti. U bir marta yaratiladi va istalgancha shadow root'ga ulanadi — nusxa olinmaydi:

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  button#theme { font: inherit; padding: 0.3rem 0.8rem; }
</style>
<dish-card>Osh</dish-card>
<dish-card>Manti</dish-card>
<dish-card>Lag'mon</dish-card>
<button id="theme" type="button">Mavzuni almashtirish</button>
<script>
  const sheet = new CSSStyleSheet();
  sheet.replaceSync(`
    :host { display: block; padding: 0.4rem 0.8rem;
      margin: 0.3rem 0; border-left: 4px solid #2e7d32;
      background: #f3f8f3; }`);

  class DishCard extends HTMLElement {
    constructor() {
      super();
      const root = this.attachShadow({ mode: "open" });
      root.adoptedStyleSheets = [sheet];
      root.innerHTML = "<slot></slot>";
    }
  }
  customElements.define("dish-card", DishCard);

  const cards = document.querySelectorAll("dish-card");
  const shared = [...cards].every(
    (card) => card.shadowRoot.adoptedStyleSheets[0] === sheet);
  console.log("Hammasida bitta obyekt:", shared);

  document.getElementById("theme").addEventListener("click", () => {
    sheet.replaceSync(`
      :host { display: block; padding: 0.4rem 0.8rem;
        margin: 0.3rem 0; border-left: 4px solid #d35400;
        background: #fdf0e6; }`);
    const color = getComputedStyle(cards[2]).borderLeftColor;
    console.log("Lag'mon ramkasi:", color);
  });
</script>

Konsolda:

text
Hammasida bitta obyekt: true

«Mavzuni almashtirish» ni bosing: uchala karta bir zumda to'q sariqqa o'tadi va konsolga Lag'mon ramkasi: rgb(211, 84, 0) chiqadi (biz bosib tekshirdik). Biz faqat bitta obyektni o'zgartirdik.

Qatorma-qator:

  • new CSSStyleSheet() — bo'sh stil jadvali.
  • sheet.replaceSync(matn) — uning ichini CSS matni bilan almashtiradi. Asinxron varianti — sheet.replace(matn), u Promise qaytaradi.
  • root.adoptedStyleSheets = [sheet] — shadow root bu jadvalni "asrab oladi" (adopt). Bu oddiy massiv: bir nechta jadval berish mumkin, tartibi muhim — keyingisi yutadi.
  • Shu xususiyat document da ham bor: document.adoptedStyleSheets — sahifaning o'ziga JavaScript'dan stil qo'shish.

Bitta cheklov: replaceSync ichida @import ishlamaydi. Chrome 154 uni tashlab yuborib, konsolda ogohlantiradi: @import rules are not allowed here. Qolgan qoidalar ishlayveradi.

Constructable stylesheets 2023-yil martidan Baseline, 2025-yil sentabridan widely available (web-features 3.40).

7.3 CSS modul skriptlari

Dinamik import darsida bir qatorni ko'rgan edik: import sheet from "./dish-card.css" with { type: "css" }. Natija — aynan shu CSSStyleSheet obyekti, uni adoptedStyleSheets ga berasiz. CSS alohida faylda turadi, muharrir uni CSS deb taniydi. Lekin u hali Safari'da yo'q — Baseline emas. Hozircha replaceSync yoki bundler (16-qismda) ishonchliroq.

8. :state() — komponentning ichki holatlari

:host([sold-out]) atribut bilan ishlaydi. Lekin ba'zi holatlar atribut bo'lmasligi kerak: masalan, "loading" yoki "selected". Ularni HTML'da ko'rsatish shart emas, sahifa ularni o'zgartirmasligi ham kerak.

Buning uchun custom state (maxsus holat) bor. Komponent ElementInternals orqali holatni qo'shadi:

js
class DishCard extends HTMLElement {
  #internals = this.attachInternals();

  set loading(value) {
    if (value) this.#internals.states.add("loading");
    else this.#internals.states.delete("loading");
  }
}

CSS esa uni :state() bilan o'qiydi — ichkaridan ham, tashqaridan ham:

css
/* komponent ichida */
:host(:state(loading)) { opacity: 0.6; cursor: progress; }

/* sahifada */
dish-card:state(loading) { outline: 2px dashed #999; }

states — Set ga o'xshash obyekt: add, delete, has. Chrome'da tekshirdik: holat qo'shilgach, ichki :host(:state(…)) ham, tashqi dish-card:state(…) ham ishladi, element.matches(":state(loading)") — true. Atributdan farqi: sahifa holatni o'qiy oladi, lekin o'zgartira olmaydi — #internals private.

:state() 2024-yil mayidan Baseline (Chrome 125, Firefox 126, Safari 17.4).

9. Ko'p uchraydigan xatolar

9.1 :host { display: block } ni unutish

Custom element — inline. Karta width olmaydi, margin yuqori-pastda ishlamaydi. Tuzatish: har blok komponentda :host { display: block; } va :host([hidden]) { display: none; }.

9.2 Ichkarini tashqi selektor bilan bezashga urinish

dish-card h3 { color: orange; } — jim. Shadow ichiga selektor kirmaydi. Tuzatish: komponent CSS o'zgaruvchisi yoki part ochgan bo'lsa — shular orqali. Ochmagan bo'lsa — muallifdan so'rang yoki boshqa komponent tanlang.

9.3 :host ni sahifa qoidasidan kuchli deb o'ylash

Muallif :host { padding: 1rem } yozdi, lekin sahifadagi dish-card { padding: 0 } uni bosib ketdi. Specificity'ni oshirish yordam bermaydi — kontekst bosqichi oldinroq. Tuzatish: bu xato emas, xususiyat: sahifa oxirgi so'zni aytadi. Muallif rostdan qat'iy bo'lishi kerak bo'lsa — !important, lekin faqat zarur joyda.

9.4 ::slotted() bilan chuqur bezash

::slotted(ul li) — yaroqsiz, butun qoida tashlanadi. Tuzatish: ::slotted(ul) va meros, yoki ichki ro'yxat uchun sahifaning o'z CSS'i — kontent baribir sahifaniki.

9.5 Shriftni shadow ichida e'lon qilish

Komponent ichidagi <style> ga @font-face yozildi, shrift chiqmadi. Chrome'da tekshirdik: shadow ichidagi @font-face document.fonts ro'yxatiga tushmadi. Tuzatish: @font-face ni sahifa CSS'ida e'lon qiling, komponent ichida faqat font-family nomini ishlating — u meros bo'lib ham o'tadi.

10. Mashqlar

1-mashq (oson): Mavzu o'zgaruvchilari

<discount-tag> ("chegirma yorlig'i") komponentini yozing: ichida <slot>, fon rangi --discount-tag-bg (zaxira #c0392b), matn rangi --discount-tag-color (zaxira white). Sahifada uch yorliq: biri zaxira ranglarda, ikkitasi .partner ichida, u yerda --discount-tag-bg: #d35400. Konsolga uchala yorliqning fon rangini chiqaring.

Ishora: «CSS o'zgaruvchilari — mavzuning asosiy yo'li» bo'limidagi kod; fon :host ga yoziladi; :host { display: inline-block; }.

Yechim
html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  .partner { --discount-tag-bg: #d35400; }
</style>
<discount-tag>-10%</discount-tag>
<p class="partner">
  <discount-tag>-15%</discount-tag>
  <discount-tag>-20%</discount-tag>
</p>
<script>
  class DiscountTag extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: "open" }).innerHTML = `
        <style>
          :host { display: inline-block; padding: 0.1rem 0.5rem;
            border-radius: 0.3rem; font-weight: 700;
            background: var(--discount-tag-bg, #c0392b);
            color: var(--discount-tag-color, white); }
        </style>
        <slot></slot>`;
    }
  }
  customElements.define("discount-tag", DiscountTag);

  for (const tag of document.querySelectorAll("discount-tag")) {
    const background = getComputedStyle(tag).backgroundColor;
    console.log(tag.textContent, background);
  }
</script>

Konsolda:

text
-10% rgb(192, 57, 43)
-15% rgb(211, 84, 0)
-20% rgb(211, 84, 0)

Fon :host ga yozilgan — getComputedStyle(tag) uni host'ning o'zida ko'radi. --discount-tag-color hech qayerda berilmagan, shuning uchun matn uchalasida ham oq (zaxira).

2-mashq (o'rta): Tugmani hamkor uslubida

Quyidagi komponentda tugmaga part="button" qo'shing. Keyin sahifa CSS'ida faqat ::part() bilan tugmani dumaloq (border-radius: 999px), to'q sariq fonli va oq matnli qiling, sichqoncha ustida esa to'qroq (#a84300). Konsolga tugmaning border-radius qiymatini chiqaring.

js
this.attachShadow({ mode: "open" }).innerHTML =
  `<button type="button">Savatga</button>`;

Ishora: ::part() dan keyin :hover mumkin.

Yechim
html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  dish-card::part(button) {
    font: inherit; padding: 0.4rem 1rem; border: 0;
    border-radius: 999px; background: #d35400; color: white;
  }
  dish-card::part(button):hover { background: #a84300; }
</style>
<dish-card></dish-card>
<script>
  class DishCard extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: "open" }).innerHTML =
        `<button type="button" part="button">Savatga</button>`;
    }
  }
  customElements.define("dish-card", DishCard);

  const button = document.querySelector("dish-card")
    .shadowRoot.querySelector("button");
  console.log("radius:", getComputedStyle(button).borderRadius);
</script>

Konsolda:

text
radius: 999px

Komponentda faqat bitta atribut qo'shildi — part="button". Qolgan hammasini sahifa hal qildi. Komponent muallifi tugma ichini keyin o'zgartirsa ham (masalan, ikonka qo'shsa), hamkorning stili buzilmaydi.

3-mashq (qiyin): Holat va umumiy stil

<order-row> ("buyurtma qatori") komponentini yozing. Talablar:

  • Barcha nusxalar bitta CSSStyleSheet ni bo'lishsin (adoptedStyleSheets).
  • Bosilganda komponent selected custom state'ini yoqib-o'chirsin (states.add / states.delete).
  • Ichki stil: :host(:state(selected)) — yashil chap ramka.
  • Sahifa stili: order-row:state(selected) — qalin shrift.
  • Sahifada ikki qator. Ikkinchisini bosgandan keyin konsolga ikkala qatorning matches(":state(selected)") qiymatini chiqaring.

Ishora: holatni almashtirish — has bilan tekshirib, add yoki delete. Bosish tinglovchisini konstruktorda this ga qo'shing.

Yechim
html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  order-row:state(selected) { font-weight: 700; }
</style>
<order-row>Osh × 2</order-row>
<order-row id="second">Manti × 1</order-row>
<script>
  const sheet = new CSSStyleSheet();
  sheet.replaceSync(`
    :host { display: block; padding: 0.3rem 0.6rem; cursor: pointer;
      border-left: 4px solid transparent; }
    :host(:state(selected)) { border-left-color: #2e7d32; }`);

  class OrderRow extends HTMLElement {
    #internals = this.attachInternals();

    constructor() {
      super();
      const root = this.attachShadow({ mode: "open" });
      root.adoptedStyleSheets = [sheet];
      root.innerHTML = "<slot></slot>";
      this.addEventListener("click", () => this.#toggle());
    }

    #toggle() {
      const states = this.#internals.states;
      if (states.has("selected")) states.delete("selected");
      else states.add("selected");
    }
  }
  customElements.define("order-row", OrderRow);

  document.getElementById("second").addEventListener("click", () => {
    for (const row of document.querySelectorAll("order-row")) {
      console.log(row.textContent, row.matches(":state(selected)"));
    }
  });
</script>

Ikkinchi qatorni bossangiz, u qalin bo'ladi va yashil ramka oladi. Konsolda:

text
Osh × 2 false
Manti × 1 true

Biz bosib tekshirdik. Komponentning click tinglovchisi sahifanikidan oldin ishladi: u konstruktorda qo'shilgan — sahifa skripti esa keyinroq. Bitta elementdagi tinglovchilar qo'shilish tartibida chaqiriladi, shuning uchun konsol yangi holatni ko'rsatdi. Klaviatura uchun qatorga tabindex="0" va role="button" hamda Enter tinglovchisi ham kerak bo'lardi — Bobur uchun buni o'zingiz qo'shib ko'ring.

4-mashq: Amaliy tajriba — ikki sayt, bitta komponent

kurs/mashqlar/13/26-mavzu/ papkasida ikki sahifa yarating: bahor.html va hamkor.html. Ikkalasi bir xil dish-card.js ni ulasin (Web Components darsining 4-mashqidagi modul). Komponentga quyidagi API'ni qo'shing va hujjatlang (fayl boshidagi izohda):

  • CSS o'zgaruvchilari: --dish-card-color (sarlavha), --dish-card-bg.
  • Part'lar: title, button.

hamkor.html faqat shu API orqali kartani to'q sariq va dumaloq tugmali qilsin. Live Server bilan ikkala sahifani yonma-yon oching.

Yechim

dish-card.js boshida izoh:

js
/**
 * <dish-card name="Osh" price="35000" [sold-out]>tavsif</dish-card>
 * CSS o'zgaruvchilari: --dish-card-color, --dish-card-bg
 * Part'lar: title, button
 * Hodisa: add-to-cart (detail: { name, price })
 */

Komponent stilida: :host { background: var(--dish-card-bg, white); }, h3 { color: var(--dish-card-color, #2e7d32); }, qolipda <h3 class="name" part="title"> va <button type="button" part="button">. hamkor.html da:

css
:root { --dish-card-color: #d35400; --dish-card-bg: #fffaf5; }
dish-card::part(button) { border-radius: 999px; border: 0;
  background: #d35400; color: white; }

Hamkor komponent kodiga tegmadi. Ertaga «Bahor» kartaga yangi maydon qo'shsa, hamkor sahifasi uni avtomatik oladi — hujjatlangan API o'zgarmaguncha.

bash
git add 13/26-mavzu
git commit -m "13/26: dish-card uchun CSS o'zgaruvchilari va part"

11. Real ishda

  • Dizayn tizimlari hujjatlari. Material Web, Spectrum, Web Awesome sahifalarida har komponent uchun "CSS custom properties" va "CSS parts" jadvallari bor — mavzu shular orqali beriladi.
  • Brendlash. Bitta vidjet (to'lov, chat, bron) o'nlab mijoz saytida ishlaydi; har mijoz o'z ranglarini o'zgaruvchilar bilan beradi.
  • Tungi rejim. :root dagi o'zgaruvchilar prefers-color-scheme bilan almashadi — barcha komponentlar o'zi moslashadi, :host-context kerak emas.
  • Intervyu. "Shadow DOM ichidagi elementni tashqaridan qanday bezaysiz?", "::part va CSS o'zgaruvchisi farqi?", ":host stili nega ishlamayapti?" — Web Components bilan ishlaydigan jamoalarda so'raladi.

Xulosa

  • Chegaradan selektorlar o'tmaydi; meros bo'ladigan xususiyatlar va CSS o'zgaruvchilari o'tadi.
  • Ichkaridan: :host, :host(selektor) — host'ni; ::slotted(selektor) — slotdagi yuqori darajadagi elementlarni bezaydi. :host { display: block } ni unutmang.
  • Tashqaridan: CSS o'zgaruvchilari (qiymatlar), ::part(nom) (butun element), ichma-ich komponentlarda exportparts.
  • Kaskadning kontekst bosqichi: oddiy qoidada tashqi yutadi, !important da ichki yutadi — specificity'dan oldin.
  • adoptedStyleSheets — bitta CSSStyleSheet ko'p shadow root'da; :state() — atributsiz ichki holatlar.

Keyingi dars: Rendering pipeline va layout thrashing — brauzer stil, joylashuv va bo'yashni qanday bajaradi, JavaScript qaysi o'qishda uni "majburan" ishlatadi va buni Chrome'da qanday o'lchaymiz.

Manbalar

  • CSS Scoping Module Level 1: :host, ::slotted(), shadow kontekst kaskadi — w3.org/TR/css-scoping-1
  • CSS Shadow Parts: ::part(), exportparts — w3.org/TR/css-shadow-parts-1
  • CSS Cascading and Inheritance Level 5: "Context" (encapsulation context) — w3.org/TR/css-cascade-5
  • MDN: :host, ::slotted(), ::part(), CSSStyleSheet(), CustomStateSet — developer.mozilla.org
  • web-features 3.40: shadow-parts, constructed-stylesheets, state, host-context — github.com/web-platform-dx/web-features
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
CSS va Shadow DOM: :host, ::slotted(), ::part() bilan web komponentlarni stillash — IlmHamroh