Stylizacja komponentów internetowych

Shadow DOM to granica stylu: reguły strony nie docierają do niego, a reguły komponentów nie wyciekają na zewnątrz, z wyjątkiem właściwości dziedziczonych (w tym właściwości niestandardowych). Możesz to zrobić za pomocą kanałów wymienionych poniżej.

: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; }

Unikaj :host-context(). Wychodzi poza granice, aby testować elementy nadrzędne, ale Firefox i Safari odmówiły jego wdrożenia, a CSSWG skłania się ku temu, aby nie rozwijać go dalej – traktuj go jako ślepy zaułek tylko dla Chromium, a nie jako opcję przyszłościową. Zamiast tego motyw jest ustawiany za pomocą odziedziczonej właściwości niestandardowej CSS (poniżej): konsument ustawia właściwość w elemencie nadrzędnym, a komponent odczytuje ją poza granicą, co działa wszędzie i utrzymuje obie strony rozdzielone.

Właściwości dziedziczone i właściwości niestandardowe przekraczają granicę

Dziedziczone właściwości CSS (color, font-family, line-height i właściwości niestandardowe CSS) przechodzą przez granicę shadow. Dzięki temu właściwości niestandardowe stają się podstawowym interfejsem API do tworzenia motywów: udostępniaj je z odpowiednimi wartościami zastępczymi.

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

Właściwość niestandardową zdefiniowaną w elemencie :host można zastąpić tylko za pomocą reguł kierowanych na element hosta. Aby umożliwić dziedziczenie i zastępowanie z dowolnego miejsca w strukturze, pozostaw właściwość publiczną nieustawioną w :host i polegaj na wartości zastępczej w każdym var(). Nie ustawiaj właściwości publicznej w :host.

Aby zadeklarować kreację zastępczą tylko raz, a nie powtarzać jej przy każdym var(), zdefiniuj prywatną właściwość „shadow” w :host, która odczytuje właściwość publiczną, i odwołuj się do właściwości prywatnej wewnętrznie:

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

Konsumenci nadal zastępują --card-bg z zewnątrz; podkreślenie na początku sygnalizuje, że alias jest wewnętrzny, a kreacja zastępcza znajduje się dokładnie w jednym miejscu.

::slotted(): stylizowanie rzutowanego modelu Light DOM

::slotted(selector) jest kierowany na węzły najwyższego poziomu przypisane do boksu. Nie może dotrzeć do elementów podrzędnych i jest zastępowana przez dowolną regułę Light DOM (użytkownik jest właścicielem własnych treści), więc używaj jej do drobnych poprawek, a nie do kontroli. ::slotted(...) !important może zastąpić reguły konsumenta, ale spowoduje to zastąpienie stylów, do których konsument ma prawo – zarezerwuj to dla poprawek strukturalnych, a nie estetycznych.

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

::part() i exportparts: udostępnianie elementów wewnętrznych na potrzeby motywów

Oznacz element wewnętrzny za pomocą part="name", aby umożliwić konsumentom stylizowanie go za pomocą dowolnej właściwości bez konieczności poznawania struktury wewnętrznej.

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

Udostępnij kilka wysokopoziomowych elementów (przycisk, pole tekstowe, etykieta), a nie każdy element <div>. Szerokie powierzchnie elementów stają się niestabilnym interfejsem API. Aby ponownie udostępnić części komponentu zagnieżdżonego w górę przez własny komponent, przekaż je za pomocą exportparts:

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

Wybierz jeden z tych 2 kanałów: właściwości niestandardowe w przypadku ograniczonego, bezpiecznego dla systemu projektowania zestawu wartości lub części w przypadku nieograniczonego stylizowania konkretnego elementu.

Przekazywanie stylów do opakowanego elementu natywnego

Gdy element niestandardowy zawiera element natywny (<my-button> wokół rzeczywistego elementu <button>), często pojawiają się 3 problemy:

  • Elementy sterujące formularza nie dziedziczą typografii. color i właściwości niestandardowe przekraczają granicę, ale <button>, <input> i <select> pobierają font i powiązane elementy z arkusza stylów UA, który nie dziedziczy. Dlatego opakowany element sterujący jest renderowany przy użyciu czcionki platformy, a nie czcionki strony, dopóki nie ustawisz w nim właściwości font: inherit (i zwykle color: inherit) w drzewie shadow. To najczęstszy błąd.
  • Style :host nie docierają do elementu wewnętrznego. Ustawienie background/border/padding w :host stylizuje pole hosta, a nie element sterujący wewnątrz niego. Przekazuj celowo: udostępnij element sterujący jako ::part, aby umożliwić dowolne stylizowanie, lub przekazuj stały zestaw wartości za pomocą właściwości niestandardowych – reguła :host nie jest kaskadowo stosowana do elementu opakowanego.
  • display: contents + all: inherit to prawdziwy kompromis, a nie skrót. Zwijanie hosta za pomocą display: contents i resetowanie wewnętrznego elementu sterującego za pomocą all: inherit to obecnie jedyny ogólny sposób przekazywania dowolnych stylów do opakowanego elementu natywnego bez konieczności poznawania przez użytkowników części poszczególnych komponentów lub właściwości niestandardowych. Usunięcie własnego pola hosta jest zwykle celem wzorca (kompozycja kompensuje brak wbudowanego dziedziczenia). Dwa prawdziwe zastrzeżenia: all: inherit usuwa wartości domyślne UA, więc ogranicz zakres do konkretnych właściwości lub użyj revert, gdy chcesz przywrócić wartości domyślne. Konkretny element blokujący – getBoundingClientRect() i getClientRects() zwracają zero dla elementów display: contents, więc pozycjonowanie kotwicy względem hosta jest uszkodzone. Jeśli te kompromisy nie są odpowiednie, użyj ::part lub właściwości niestandardowych.

:state(): stylizowanie stanów komponentu,

Element niestandardowy może ujawniać wewnętrzne stany logiczne za pomocą ElementInternals przez this.#internals.states.add('busy') i .delete('busy') (informacje o użyciu cyklu życia znajdziesz w artykule o elementach niestandardowych). Te stany stają się kanałem stylizacji poza granicą – podobnie jak ::part i właściwości niestandardowe, ale w przypadku stanu, a nie struktury lub wartości. Dopasuj je w drzewie shadow za pomocą :host(:state(name)), a w arkuszu stylów klienta za pomocą 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); }

W przeciwieństwie do atrybutu odzwierciedlonego stan niestandardowy jest tylko do odczytu na zewnątrz: konsumenci mogą go stylizować, ale nie mogą go ustawiać ani fałszować, więc komponent pozostaje jedynym zapisującym. Używaj go zamiast atrybutu data-*, gdy stan jest własnością komponentu i wymaga jedynie stylizacji.

@layer i zapytania o kontenery wewnątrz komponentów

  • Warstwy kaskadowe (@layer) działają w obrębie korzenia shadow i są do niego ograniczone. Nazwa warstwy w drzewie shadow jest niezależna od warstwy o tej samej nazwie na stronie. Używaj ich do porządkowania własnych stylów komponentów (np. @layer base, theme;), aby zastąpienia konsumenckie za pomocą właściwości niestandardowych i ::part nadal działały w przewidywalny sposób.
  • Zapytania o kontener to odpowiednie narzędzie do tworzenia komponentów elastycznych: komponent powinien reagować na przestrzeń, którą ma do dyspozycji, a nie na widoczny obszar. Zadeklaruj hosta lub kod otaczający jako kontener (nadając elementowi :host pole układu, np. display: block, ponieważ elementy niestandardowe domyślnie mają wartość display: inline, a ograniczenie rozmiaru nie dotyczy elementów inline ani contents) i wyślij do niego zapytanie, aby ten sam komponent dostosowywał się do paska bocznego lub obszaru o pełnej szerokości.
: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; }
}

Strategie zastępcze

::part, warstwy kaskadowe i zapytania o kontenery są powszechnie dostępne. Motywy są stosowane głównie za pomocą dziedziczonych właściwości niestandardowych, które działają nawet tam, gdzie te funkcje nie są dostępne. Jak wspomniano powyżej, nie polegaj na :host-context() – jest to rozwiązanie przeznaczone tylko dla Chromium i nie jest to opcja przyszłościowa.