19 min. czytania

Każdy element DOM-u rzuca cień

Shadow DOM daje komponentom ich własny, izolowany świat — lokalne style, enkapsulację znaczników i granicę, która utrzymuje wszystko na swoim miejscu.

Zaktualizowano: 18 maja 2026
Cień drzewa rzucony na zielony trawnik z jesiennym lasem w tle
Zdjęcie autorstwa Ricardo Gomez Angel na Unsplash

W zasadzie „nie każdy” — pozwól, że wyjaśnię. Strony internetowe składają się z elementów HTML, takich jak <article> czy <div>, które tworzą strukturę podobną do drzewa nazwaną Document Object Model (DOM). Możemy ją nazywać regularnym lub jasnym DOM-em. To pewnie jest dla ciebie jasne. Jednakże te elementy mają ciemne przeciwieństwo — shadow DOM.

Czym jest shadow DOM?

Shadow DOM jest drugim filarem komponentów internetowych. Aby zobaczyć pierwszy, sprawdź mój poprzedni post — Wprowadzenie do web components — czym są custom elements?

Cienisty DOM (ang. shadow DOM) jest mechanizmem enkapsulacji, który możesz dobrowolnie włączyć. Możesz o nim myśleć, jak o prywatnym drzewie DOM, gdzie jego wnętrze jest ukryte przed językiem JavaScript lub CSS na stronie. Shadow DOM może mieć dwa źródła:

  1. Shadow DOM stworzony przez użytkownika. Wyraźnie wtedy dołączasz korzeń (ang. shadow root) do elementu, wykorzystując odpowiednie API.
  2. Shadow DOM stworzony przez przeglądarkę. Niektóre wbudowane elementy, jak <input> lub <select>, wewnętrznie używają cienistego DOM-u, stworzonego przez przeglądarkę.

Które elementy rzucają shadow DOM?

Nie „każdy”, ale wiele HTML-owych elementów może być hostem dla cienistego DOM-u. Możemy tu wyróżnić dwie kategorie, które łączą się ze źródłami powyżej:

  1. Elementy niestandardowe. Każdy autonomiczny niestandardowy element może mieć swój shadow DOM. Jest to główny przypadek użycia — twoje własne, niestandardowe komponenty. Mój poprzedni wpis dokładnie opisuje custom elements.
  2. Wbudowane elementy. Istnieje lista konkretnych, wbudowanych elementów HTML, które wspierają shadow DOM. Są to elementy takie jak <article>, <div>, <span> czy <section>. Istnieje lista wszystkich elementów wspierających shadow DOM.

Co dostarcza shadow DOM?

Dlaczego mielibyśmy go używać poza fajną nazwą? Shadow DOM dostarcza kilka kluczowych funkcjonalności dla web components:

  • Enkapsulacja DOM-u. Cieniste drzewo jest odizolowane od głównego dokumentu. Nie można dostać się do jego wewnętrznych elementów standardowymi metodami, takimi jak document.querySelector(). Możemy nawet zablokować bezpośredni dostęp do cienistego korzenia, wykorzystując tryb zamknięty, closed.
  • Enkapsulacja styli. Style CSS są wyraźnie rozdzielone pomiędzy takim komponentem i stroną. Może to zapobiec przypadkowym wyciekom.
  • Slots. Cieniste drzewo wspiera symbole zastępcze nazwane „slotami”, gdzie treści jasnego DOM-u mogą zostać wyświetlone.
  • Zmiana celu zdarzeń (ang. event retargeting). Zdarzenia z shadow roota są dostępne od zewnątrz, ale ze zmienionym celem. Oznacza to, że kiedy zdarzenia wypływają z tego korzenia, ich cel jest zmieniony na element hosta, ukrywając wewnętrzny element, który autentycznie je wywołał.

Dwie składnie shadow DOM-u

Aktualnie istnieją dwa sposoby, aby stworzyć shadow DOM.

  1. Imperatywnie w języku JavaScript wywołując metodę attachShadow().
  2. Deklaratywnie w znacznikach HTML. Jest to nowy dodatek do API.

Nie lubię stwarzać napięcia jak w „tureckich awanturach”, ale zaplanowałem już następny post, gdzie dokładnie opiszę to podejście deklaratywne. Tutaj rozwinę temat oryginalnej składni.

JS
const host = document.querySelector('#my-element')
const shadow = host.attachShadow({ mode: 'open' })

shadow.innerHTML = `<p>Hello from the shadows</p>`

Możesz wywołać metodę attachShadow() na dowolnym, obsługiwanym elemencie i posłać jej opcje w formie obiektu. W przykładzie powyżej dołączyliśmy paragraf do elementu my-element. Metoda zwraca ShadowRoot — specjalny fragment dokumentu (DocumentFragment), który zachowuje się jak korzeń dla cienistego drzewa. Jak widzisz, możesz wypełniać to drzewo podobnie do standardowego DOM-u, używając właściwości i metod, takich jak innerHTML lub appendChild(). Po dołączeniu, cieniste drzewo zastępuje dzieci hosta podczas renderowania (chyba że wykorzystasz sloty, aby je przywrócić).

Tryby shadow DOM

Jedynym, wymaganym ustawieniem jest tryb, mode. Mamy tu dwie opcje:

  1. mode: 'open': element.shadowRoot zwraca obiekt ShadowRoot. Zewnętrzny kod może odczytywać i modyfikować shadow tree.
  2. mode: 'closed': element.shadowRoot zwraca null zamiast shadow roota. Wnętrze hosta jest ukryte przed zewnętrznym kodem JavaScript.

Blokowanie zapytań JS nie oznacza, że tryb closed jest całkowicie bezpieczny. Komponenty shadow DOM nadal dzielą ten sam kontekst wykonywania (ang. execution context). Jeżeli potrzebujesz bezpiecznej granicy dla zewnętrznego, niezaufanego skryptu, użyj <iframe>. Shadow DOM umożliwia enkapsulację — nie jest to środek bezpieczeństwa.

Opcjonalne ustawienia w shadow DOM

Poza ustawianiem trybu możesz kontrolować także inne zachowania cienistego korzenia, wykorzystując dodatkowe opcje:

  • delegatesFocus: Opcja ta pozwala kontrolować zachowanie focusa dla twoich komponentów. Przy ustawieniu true, kliknięcie gdziekolwiek na hosta lub używanie JavaScriptu do ustawienia na niego focusa, automatycznie ustawia focus na pierwszy dostępny element wewnątrz cienistego DOM-u.
  • slotAssignment: Kontroluje w jaki sposób dzieci z jasnego DOM-u są przypisywane do elementów <slot>. Domyślny tryb "named" automatycznie dopasowuje dzieci do slotów bazując na ich atrybutach slot="name". Tryb "manual" wyłącza to automatyczne dopasowanie — zamiast tego, manualnie dopasowujesz elementy wykorzystując slot.assign(element). Daje ci to kontrolę w rozmieszczaniu treści.
  • clonable: Gdy ustawione na true, klonowanie elementu hosta (poprzez cloneNode() lub importNode()) zawiera także shadow root. Domyślnie ustawione na false — standardowo klonowanie hosta tworzy kopię bez zawartości cienistego drzewa.
  • serializable: Gdy ustawione na true, shadow root może być serializowany do HTML-u wykorzystując element.getHTML({ serializableShadowRoots: true }). Łączy to światy deklaratywny i imperatywny — możesz stworzyć cienisty korzeń w JS-ie, a później wyeksportować go jako HTML zawierający <template shadowrootmode>.

Shadow boundary

Wcześniej widzieliśmy, że shadow DOM izoluje swoje wnętrze od reszty dokumentu. Możesz myśleć o tym, jak o ścianie pomiędzy jasnym a cienistym DOM-em. Ta ściana ma nazwę — cienista granica (ang. shadow boundary). Też brzmi nieźle, nie? Jak jakieś DLC do Elden Ringa. W każdym razie shadow boundary to granica pomiędzy jasnym a cienistym DOM-em, stworzona podczas dołączania cienistego korzenia do hosta. Ta granica to mechanizm enkapsulacji wymuszający separację na trzech głównych poziomach: CSS, DOM i zdarzeń.

Diagram przedstawiający shadow boundary

Enkapsulacja nie oznacza, że wszystko jest odizolowane. Niektóre rzeczy intencjonalnie przekraczają granicę. Opiszemy te poziomy dokładnie, ponieważ nie jest oczywiste, co przekracza granicę, a co nie. Zamiast wyobrażać sobie solidną ścianę, możesz sobie wyobrazić ścianę z cegieł z kilkoma pęknięciami tu i ówdzie.

CSS i shadow boundary

Globalne selektory CSS nie przekraczają granicy w żadnym kierunku — twoja strona nie wpływa na wnętrze shadow DOM-u, a style z wewnątrz nie wyciekają. Jest to duża zaleta używania cienistego DOM-u. Niedziedziczone właściwości CSS nie przekraczają granicy, podobnie jak nie są dziedziczone w jasnym DOM-ie. Jednakże dziedziczone właściwości CSS, jak color czy font-family przepływają naturalnie, bo cieniste elementy najwyższego poziomu dziedziczą po hoście.

Zmienne CSS przebijają granicę zgodnie z założeniami, będąc rekomendowanym sposobem na stylowanie komponentów internetowych. Dodatkowo pseudoelementy ::part() i ::slotted() zachowują się jak kontrolowane włazy ewakuacyjne, pozwalając autorowi zdecydować, które wewnętrzne elementy mogą być stylowane od zewnątrz.

Co?Przekracza?Notatki
Globalne selektory (div, .class, #id)NieTe selektory nie przekraczają granicy w żadnym kierunku.
Niedziedziczone właściwości (background, border, etc.)NieStandardowe zachowanie CSS — te właściwości nie są dziedziczone w żadnym DOM-ie.
Dziedziczone właściwości (color, font-family, etc.)TakCieniste elementy najwyższego poziomu dziedziczą po hoście.
Zmienne CSS (--my-var)TakCelowo przekraczają granicę. Rekomendowany sposób stylowania.
::part()ZależyTylko jeżeli autor komponentu ustawi na elemencie part="name".
::slotted()ZależyStyluje elementy bezpośrednio w slotach (bez potomków).

Przestarzałe kombinatory, jak /deep/ czy ::shadow, zostały usunięte ze wszystkich przeglądarek.

DOM i shadow boundary

Standardowe zapytania DOM-owe jak document.querySelector() nie wnikają do shadow roota — granica wycina je całkowicie. Aby dostać się do jego wnętrza, potrzebujesz przejść przez element.shadowRoot.querySelector(), a i wtedy działa to tylko dla otwartego korzenia. ID mają także zakres roota, co oznacza, że bezpiecznie możesz używać tego samego ID w wielu komponentach bez kolizji. Jedynym wyjątkiem dla tej enkapsulacji jest <slot> — rzutuje on dzieci jasnego DOM-u w cienistym drzewie DOM z powodu renderowania, pomimo że technicznie te elementy nadal żyją w jasnym DOM-ie. Jeżeli brzmi to skomplikowanie, nie martw się — rozwiniemy temat slotów w następnej sekcji.

Co?Przekracza?Notatki
Zasięg IDNieID wewnątrz shadow roota mają tylko jego zakres.
document.querySelector()NieNie znajduje elementów wewnątrz shadow roota.
element.shadowRootZależyZwraca ShadowRoot w trybie otwartym, a null w zamkniętym.
shadowRoot.querySelector()ZależyDziała tylko dla shadow roota z mode: "open".
<slot> content projectionTakDzieci jasnego DOM-u są renderowane wewnątrz cienistego, ale pozostają w jasnym.

Zdarzenia i shadow boundary

Większość wbudowanych zdarzeń przekracza granicę (z wyjątkami dla zdarzeń, które nie są skomponowane i nie bąbelkują). Niestandardowe zdarzenia w domyśle nie przekraczają granicy, ale możesz je zmodyfikować, żeby przekraczały. Gdy ustawisz opcję composed na true, zdarzenia propagują przez shadow boundary. Opcja bubbles: true włącza propagację w hierarchii DOM dla obu typów.

Co?Przekracza?Notatki
load, unload, abort, error, selectNieNieskomponowane.
mouseenter, mouseleave, pointerenter, pointerleaveNieSkomponowane, ale niebąbelkujące—w praktyce nie przekraczają granicy.
slotchangeNieBąbelkujące, ale nieskomponowane.
Większość zdarzeń UI (click, keydown, input, etc.)TakSkomponowane domyślnie—bąbelkują przez granicę.
Niestandardowe zdarzenia (new CustomEvent(...))Zależycomposed: true jest wymagane, aby przekroczyć granicę. Dodaj bubbles: true, jeśli zdarzenie ma też bąbelkować w górę.

Nawet jeżeli zdarzenie przekroczy granicę, event.target jest zmieniony na element hosta, aby kapsułkować detale implementacyjne. Metoda event.composedPath() zwraca wszystkie węzły (ang. nodes), przez które zdarzenie propaguje, chyba że tryb shadow roota ustawiony jest na closed — wtedy pomija wnętrze tego cienistego drzewa.

Co?Przekracza?Notatki
event.target (retargeting)TakKiedy zdarzenie przekracza granicę, target jest zmieniony na element hosta.
event.composedPath()ZależyZwraca całą ścieżkę włączając shadow boundary (w trybie otwartym).

ARIA i shadow boundary

Niestety, największym problemem jest aktualnie dostępność. Jak widzieliśmy, ID mają ograniczony zasięg, uniemożliwiając odwołania przez granicę. Aktualnie jedynym, częściowym rozwiązaniem jest właściwość ariaLabelledByElements. Jest to właściwość w języku JavaScript, która akceptuje odniesienia do elementów, omijając ten problem zasięgu. Jest wspierana przez wszystkie największe przeglądarki od 2025 roku, ale nadal ma ograniczenia — element referencyjny musi być w tym samym lub nadrzędnym cienistym korzeniu i nie wspiera renderowania po stronie serwera. Tak więc aktualnie nie ma jeszcze czystego rozwiązania. Istnieje propozycja celu referencyjnego, więc ekosystem zmierza w kierunku czystego rozwiązania.

Co?Przekracza?Notatki
aria-labelledbyNieOdwołania ID są ograniczone do shadow roota.
aria-describedbyNieTo samo ograniczenie co aria-labelledby.
for (label)NieAsocjacja <label for="id"> nie zadziała jeżeli <input> jest wewnątrz shadow roota.
ariaLabelledByElementsZależyWłaściwość która akceptuje odnośniki do elementów zamiast ID, omijając problem ograniczenia z zakresem. Działa jednak tylko wtedy, gdy element referencyjny jest w tym samym bądź nadrzędnym korzeniu.

Jest tu wiele do przyswojenia, więc nie powinieneś próbować zapamiętywać każdego detalu. Zapamiętaj, że ta cienista granica działa jak lustro weneckie. Dziedziczone style czy zmienne CSS wpływają do wewnątrz, umożliwiając stylowanie komponentów. Niektóre zdarzenia wypływają na zewnątrz, ale ich cel jest zmieniony, żeby ukryć wnętrzności. Standardowe zapytania DOM są całkowicie blokowane. Powoduje to problemy z dostępnością, ale na horyzoncie widnieją rozwiązania.

Projekcja treści ze slotami

Cieniste drzewo zastępuje dzieci hosta podczas renderowania. Bez żadnego mechanizmu do omijania tego zachowania, nie mielibyśmy dynamicznych treści — wszystko byłoby zapisane na sztywno w cienistym drzewie. Tym mechanizmem jest projekcja treści.

Projekcja treści (ang. content projection) jest procesem, w którym bierze się dzieci z jasnego DOM-u i renderuje je w odpowiednich miejscach wewnątrz shadow tree. Element <slot> jest narzędziem, które to umożliwia. Trzyma on miejsce dla jasnych dzieci wewnątrz cienistego drzewa. Podczas kompozycji (ang. composition), przeglądarka patrzy na dzieci hosta jasnego DOM-u i umieszcza je w pasujących slotach. W jej wyniku użytkownik widzi spłaszczoną strukturę DOM, składającą się z wszystkich tych elementów. Zaktualizujmy nasz poprzedni diagram o sloty.

Diagram projekcji treści w shadow DOM

Elementy wsunięte w ten sposób pozostają nadal w jasnym DOM-ie — nie są w cienistym drzewie. Są po prostu renderowane w miejscach slotów. Jest to istotne, bo takie elementy mogą być ostylowane przez CSS strony, a nie te style cienistego drzewa.

Typy slotów

Jeżeli szukasz analogii, to sloty są podobne do props.children w świecie Reacta. Ponadto system slotów Vue był zainspirowany przez specyfikację slotów cienistego DOM-u. I podobnie, mamy tu dwa typy slotów:

  1. Domyślne
  2. Nazwane

Domyślne sloty

Domyślny slot (ang. default slot) to element <slot> bez atrybutu name. Łapie on wszystkie dzieci jasnego DOM-u, które nie mają atrybutu slot. Koncepcyjnie, działa on podobnie jak parametr rest, który zbiera pozostałe argumenty (...rest).

HTML
<!-- Shadow tree (wewnętrzny szablon komponentu) -->
<div>
  <slot>A default value</slot>
</div>

<!-- Użycie (co dodaje konsument) -->
<my-card>
  <p>This ends up in the default slot</p>
  <p>So does this</p>
</my-card>

<!-- Spłaszczony DOM (co renderuje przeglądarka) -->
<my-card>
  <div>
    <p>This ends up in the default slot</p>
    <p>So does this</p>
  </div>
</my-card>

Jeżeli <my-card> nie ma dzieci, „A default value” zostanie wyświetlone. A jeżeli mamy wiele nienazwanych slotów, tylko pierwszy z nich otrzyma treści — reszta jest ignorowana.

Nazwane sloty

Element <slot> z atrybutem name staje się nazwanym slotem (ang. named slot). Pozwala on dokładnie kontrolować rozmieszczanie treści. Dzieci jasnego DOM-u używają odpowiadającego atrybutu slot. Każde dziecko jest rzutowane do pasującego, nazwanego slota.

HTML
<!-- Shadow tree (wewnętrzny szablon komponentu) -->
<header>
  <slot name="title">Default title</slot>
</header>
<div>
  <slot name="content"></slot>
</div>
<footer>
  <slot name="actions"></slot>
</footer>

<!-- Użycie (co dodaje konsument) -->
<my-card>
  <h2 slot="title">Hello</h2>
  <p slot="content">Some text here</p>
  <button slot="actions">Click me</button>
</my-card>

<!-- Spłaszczony DOM (co renderuje przeglądarka) -->
<my-card>
  <header>
    <h2>Hello</h2>
  </header>
  <div>
    <p>Some text here</p>
  </div>
  <footer>
    <button>Click me</button>
  </footer>
</my-card>

Oba typy slotów wspierają domyślne wartości, które są wartościami zapasowymi w przypadku brakujących dzieci w jasnym drzewie DOM. Tą wartością może być cokolwiek: tekst, elementy czy nawet zagnieżdżone komponenty. Ponadto oba typy mogą być używane razem.

HTML
<!-- Shadow tree -->
<header>
  <slot name="title">Default title</slot>
</header>
<div>
  <slot></slot>
</div>

<!-- Użycie -->
<my-card>
  <h2 slot="title">Custom title</h2>
  <p>This goes to the default slot</p>
  <p>So does this</p>
</my-card>

<!-- Spłaszczony DOM -->
<my-card>
  <header>
    <h2>Custom title</h2>
  </header>
  <div>
    <p>This goes to the default slot</p>
    <p>So does this</p>
  </div>
</my-card>

API slotów

Sloty nie muszą być statyczne — istnieje API umożliwiające interakcje z nimi.

  • slot.name: Nazwa slotu dla nazwanych slotów. Pusty łańcuch znaków w przypadku domyślnych slotów.
  • slot.assignedNodes({ flatten: false }): Zwraca wszystkie węzły jasnego DOM-u przypisane do danego slota. Z opcją { flatten: true }, zwraca także zagnieżdżone sloty i treści zapasowe, gdy nic nie jest przypisane. Domyślnie opcja flatten ma wartość false.
  • slot.assignedElements({ flatten: false }): Działa jak ta powyższa metoda, ale ogranicza się do węzłów typu element (bez typów tekstowych). Opcja flatten działa tak samo.
  • element.assignedSlot: Odwrotność — właściwość ta zwraca, do którego slotu przypisany jest dany element jasnego DOM-u. Zwraca null jeżeli element nie jest przypisany do slota lub shadow root jest zamknięty.
  • slot.assign(node1, node2, ...): Manualnie przypisuje węzły do slota. Shadow root musi być stworzony z opcją slotAssignment: "manual", aby ta metoda zadziałała. Wyrzuca błąd jeżeli jest wywołana na automatycznym slocie.
  • slotchange (zdarzenie): <slot> wywołuje to zdarzenie, kiedy przypisane węzły się zmieniają (zostają dodane, usunięte, przydzielone gdzie indziej). Nie zostaje wywołane, gdy treści wewnątrz przypisanego węzła się zmieniają.
JS
slot.addEventListener('slotchange', (e) => {
  const assigned = e.target.assignedElements()
  console.log('Slot teraz zawiera:', assigned)
})

Zdarzenie slotchange może zostać wywołane podczas fazy parsowania HTML-a, przed connectedCallback, więc musisz dodać odpowiednią logikę ochronną do swoich funkcji.

Stylowanie shadow DOM-u

Podczas stylowania cienistych komponentów, musisz pamiętać o dwóch perspektywach.

  1. Stylowanie od wewnątrz: Jak ty jako autor stylujesz komponenty.
  2. Stylowanie od zewnątrz: Jak dostarczasz API do konsumentów, aby mogli dostosować wygląd.

Stylowanie od wewnątrz

Autor tworzy komponenty internetowe. Ty, jako autor, znasz wnętrze takiego komponentu i kontrolujesz, co ma być wystawione na zewnątrz. Twoje komponenty powinny wystawiać API do stylowania i definiować, co może być zmienione (a co nie). Poniższe narzędzia odzwierciedlają tę abstrakcję.

Pseudo klasa :host pozwala ci stylować niestandardowe elementy (shadow host).

CSS
:host {
  display: block;
  padding: 16px;
  border: 1px solid gray;
}

Forma funkcjonalna pozwala ci stylować hosta warunkowo, bazując na atrybutach, klasach czy stanach.

CSS
:host([disabled]) {
  opacity: 0.5;
  pointer-events: none;
}

:host(.dark) {
  background: #333;
  color: white;
}

Inna pseudo klasa, :host-context(), pozwala aplikować style na podstawie tego, co jest w jasnym DOM-ie powyżej. W poniższym przykładzie ciemny kolor tła jest zaaplikowany tylko wtedy, gdy którykolwiek z przodków ma klasę ciemnego motywu.

CSS
:host-context(.dark-theme) {
  background: #333;
}

W momencie pisania tego posta, pseudo klasa :host-context() nie jest wspierana w Safari ani Firefoxie. Tutaj możesz sprawdzić aktualne wsparcie.

Istnieje także możliwość stylowania wspomnianej wcześniej treści w slotach. Pseudo element ::slotted() namierza te dzieci jasnego DOM-u, które zostały rzutowane na sloty.

CSS
::slotted(h2) {
  color: navy;
  margin: 0;
}

::slotted([slot='title']) {
  font-weight: bold;
}

Pseudo element ::slotted() wspiera tylko proste selektory — bez kombinatorów czy łączenia. Dlatego łączone selektory, takie jak ::slotted(article h2) lub ::slotted(h2) span nie będą działać.

Aby pozwolić konsumentom ostylować wnętrze komponentu, możesz wystawić elementy z atrybutem part — omówimy to w następnej sekcji.

Stylowanie od zewnątrz

Konsument używa komponentów internetowych. Jako konsument, nie potrzebujesz znać wnętrza komponentów. Są one czarnymi skrzynkami. Możesz wykorzystać wystawione API, aby dostosować te komponenty, bez znajomości detali implementacyjnych.

W sekcji CSS i shadow boundary widzieliśmy, że właściwości takie jak color, font-family, font-size i line-height automatycznie przepływają do cienistego drzewa. Komponent może zablokować to zachowanie używając :host { all: initial; }. Jednakże, all: initial nie resetuje zmiennych. Całkowite blokowanie dziedziczenia także nie jest rekomendowane.

Zmienne CSS są natomiast rekomendowanym sposobem stylowania komponentów internetowych. Jak wspomniałem wcześniej, wnikają do cienistego DOM-u.

CSS
/* Konsument ustawia zmienną */
my-card {
  --card-bg: #f0f0f0;
  --card-padding: 16px;
}

/* Komponent używa jej wewnętrznie */
:host {
  background: var(--card-bg, white);
  padding: var(--card-padding, 8px);
}

Autor komponentu może oznaczyć wewnętrzne elementy atrybutem part="name". Podczas gdy konsument może je namierzać za pomocą pseudo elementu ::part(). Ta składnia pozwala stylować wnętrze komponentu.

HTML
<!-- Wewnątrz shadow tree -->
<header part="header">...</header>
<div part="body">...</div>
CSS
/* Konsument styluje wystawione części */
my-card::part(header) {
  background: navy;
  color: white;
}

my-card::part(body):hover {
  opacity: 0.8;
}

Pseudo element ::part() ma podobne ograniczenia co ::slotted(). Przykładowo, nie pozwala na łączenie selektorów takich jak ::part(header) span. Jednak w przeciwieństwie do ::slotted(), wspiera pseudo klasy, jak :hover i pseudo elementy, jak ::before. Jest to celowe ograniczenie, aby zachować abstrakcję.

Składnia ::part() działa tylko do głębokości jednego poziomu. Aby stylować wewnętrzne komponenty tych zewnętrznych, skorzystaj z atrybutu exportparts. Celowo przekazuje on konkretne, wewnętrzne części, aby były dostępne z zewnątrz. exportparts działa podobnie do przekazywania propsów w React.

HTML
<!-- Shadow tree inner-card -->
<header part="header">Title</header>
<div part="body">Content</div>

<!-- Shadow tree outer-card -->
<div part="wrapper">
  <inner-card exportparts="header, body"></inner-card>
</div>
CSS
/* Teraz konsument może stylować części inner-card przez outer-card */
outer-card::part(header) {
  color: navy;
}

outer-card::part(body) {
  padding: 16px;
}

outer-card::part(wrapper) {
  border: 1px solid gray;
}

Dodatkowo możesz także przemianować części podczas przekazywania. Tutaj możemy poczynić analogię do reeksportowania modułów JavaScript. Mając takie mapowanie, unikasz kolizji nazw.

HTML
<!-- Shadow tree outer-card -->
<header part="header">Outer header</header>
<inner-card exportparts="header: inner-header"></inner-card>
CSS
/* Teraz są rozróżnialne */

/* Własny header outer-card */
outer-card::part(header) {
}

/* Przekazany header inner-card */
outer-card::part(inner-header) {
}

Główne sposoby stylowania komponentów

Istnieją dwa główne sposoby stylowania komponentów internetowych (wiele dwójek w tym wpisie, Jezu).

  1. Elementy stylów.
  2. Konstruowane arkusze stylów.

Elementy stylów

Najprostszym podejściem jest dodanie tagów <style> bezpośrednio w shadow tree. Zakres takich stylów jest automatycznie ograniczony. Jeżeli kojarzysz Styled Components, ta składnia powinna ci się wydać znajoma (chociaż działają one inaczej).

JS
shadow.innerHTML = `
  <style>
    p { color: red; }
    .card { border: 1px solid gray; }
  </style>
  <div class="card"><p>Hello</p></div>
`

Ogólne selektory, jak p w przykładzie powyżej, celują tylko w elementy wewnątrz shadow roota.

Konstruowane arkusze stylów

Możesz także stworzyć arkusze stylów w JavaScripcie i dodać je do konkretnego shadow roota (lub dokumentu). Po pierwsze, tworzysz arkusz wykorzystując konstruktor CSSStyleSheet, zastępujesz jego zawartość niestandardowymi stylami i na koniec przypisujesz go do shadow roota wykorzystując właściwość adoptedStyleSheets.

JS
const shared = new CSSStyleSheet()
shared.replaceSync(`
  :host { display: block; }
  p { color: red; }
`)

shadow.adoptedStyleSheets = [shared]

Właściwość ta przyjmuje tablicę, więc możesz przypisać wiele arkuszy stylów do jednego roota. Składnia ta rozwiązuje problem duplikacji stylów — komponenty nie potrzebują mieć podobnych stylów wewnątrz swoich tagów <style>. Wiele komponentów może dzielić obiekt arkusza ze wspólnymi stylami.

Importowanie modułów CSS

Istnieje także współczesne ulepszenie poprzedniego podejścia. Możesz załadować zewnętrzne pliki CSS jako moduły, zamiast tworzyć je w JavaScripcie. Import attributes i słowo kluczowe with mówią przeglądarce, żeby interpretowała zaimportowany plik jako moduł CSS.

JS
import styles from './my-card.css' with { type: 'css' }

shadow.adoptedStyleSheets = [styles]

Importowanie plików CSS w ten sposób nie powinno zaskakiwać — podobna składnia istnieje we frameworkach czy CSS modules.

Pomimo że importowanie modułów CSS ma wiele zalet w stosunku do konstruowania arkuszy stylów, jest jeden haczyk — wsparcie przeglądarek. Safari aktualnie nie wspiera takiej składni. Jeżeli potrzebujesz wsparcia Safari już dziś, wykorzystaj bundler taki jak Vite.

Podsumowanie

W tym wpisie uczyliśmy się o shadow DOM-ie — drugim filarze web components. Powinno być dla ciebie jasne czym shadow DOM jest, jak go użyć i jak się łączy z innymi, istniejącymi technologiami webowymi. W następnym wpisie skupimy się na nowej deklaratywnej składni shadow DOM-u i podsumujemy tę miniserię. Jeżeli chcesz dowiedzieć się więcej o cienistym DOM-ie, sprawdź poniższe źródła.

Wesprzyj mnie

Moją stronę napędza Next.js, a mnie napędza kawa. Możesz mi postawić jedną, jeżeli chcesz utrzymać ten węglowo-krzemowy system w działaniu. Ale nie czuj się do tego zobligowany. Dzięki!

Postaw mi kawę

Newsletter, który rozpala ciekawość💡

Subskrybuj mój newsletter, aby otrzymywać comiesięczną dawkę:

  • Nowości, przykładów, inspiracji ze świata programowania i designu
  • Teorii naukowych i sceptycyzmu
  • Moich ulubionych źródeł, idei, narzędzi i innych interesujących linków
Nie jestem nigeryjskim księciem, aby oferować ci okazje. Nie wysyłam spamu. Anuluj kiedy chcesz.

Pozostań ciekawy. Przeczytaj więcej

Połowa płyty winylowej na białym tle
7 min. czytania

Dostępne animacje w React

Czyli jak nie kręcić swoimi użytkownikami (jak winylem). Niektóre animacje mogą powodować problemy u użytkowników. Zadbamy o nich i sprawimy, że nieistotne animacje będą opcjonalne.

Czytaj wpis
Zdjęcie zmiennych CSS w edytorze Visual Studio Code
7 min. czytania

Konwertowanie tokenów projektowych na zmienne CSS z Node.js

Konwertowanie tokenów projektowych jest procesem podatnym na błędy — przekonałem się o tym na własnej skórze. Dlatego stworzyłem prosty skrypt dla środowiska Node.js, który pomoże mi z tym zadaniem.

Czytaj wpis
Pięć metalowych kół zębatych na czarnym tle
10 min. czytania

Gatsby z Netlify CMS

W tym wpisie przyjrzymy się bliżej Netlify CMS. Jest to przykład CMSa nowego typu, który jest oparty o Git. Zintegrujemy go z przykładowym projektem Gatsby.

Czytaj wpis