Stile dello Shadow DOM
Stila i web component con lo Shadow DOM: :host, :host(), :host-context(), ::slotted(), proprietà personalizzate CSS e adoptedStyleSheets.
Lo Shadow DOM fornisce a un componente il proprio albero DOM privato e i propri stili privati. Questa pagina si concentra sulla parte relativa agli stili: come il CSS scritto all'interno di uno shadow root è incapsulato, i selettori speciali per raggiungere l'host e il contenuto slottato (:host, :host(), :host-context(), ::slotted()), come consentire al CSS esterno di personalizzare un componente in modo intenzionale (proprietà personalizzate) e i due modi per allegare un foglio di stile (<style> vs adoptedStyleSheets).
Se lo Shadow DOM è ancora nuovo per te, leggi prima JavaScript Shadow DOM e consulta Web Components per avere un quadro più ampio di dove si inseriscono gli shadow root.
Perché gli stili sono incapsulati
La promessa fondamentale dello Shadow DOM è un confine di stile bidirezionale:
- Il CSS esterno non filtra all'interno. Una regola
p { color: red }definita a livello di pagina non toccherà un<p>all'interno di uno shadow root. Questo è ciò che rende i componenti sicuri da inserire in qualsiasi pagina. - Il CSS interno non filtra all'esterno. Gli stili in uno shadow root si applicano solo all'interno di quella radice, quindi puoi usare selettori brevi e generici (
button,p,.title) senza preoccuparti di conflitti con la pagina host.
Questo è diverso dal modello tradizionale di stili e classi, dove ogni selettore compete in un unico scope globale. All'interno di uno shadow root, lo scope è la condizione predefinita.
Creare uno shadow root
Per iniziare, collega uno shadow root a un elemento host. Tutto ciò che inserisci al suo interno — markup e CSS — è incapsulato.
<body>
<div id="my-element"></div>
<script>
// Creating Shadow DOM
const shadowRoot = document.getElementById('my-element').attachShadow({ mode: 'open' });
// Styling Shadow DOM
shadowRoot.innerHTML = `
<p>A simple shadow root content.</p>
`;
</script>
</body>Qui colleghiamo uno shadow root con il metodo attachShadow() e impostiamo il suo mode su 'open', il che ti consente di rileggere la radice in seguito tramite element.shadowRoot. ('closed' la nasconde agli script esterni, ma non aggiunge una vera sicurezza.)
Aggiungere stili con scope tramite <style>
Il modo più semplice per stilare uno shadow root è inserire un elemento <style> al suo interno. Quelle regole si applicano solo all'interno della radice — e le regole della pagina rimangono fuori.
<div id="my-element">
<!-- Shadow DOM content -->
</div>
<script>
const shadowRoot = document.getElementById('my-element').attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
/* Scoped styles */
:host {
display: block;
border: 2px solid #333;
padding: 10px;
}
p {
color: blue;
}
</style>
<p>This paragraph is styled within the Shadow DOM.</p>
`;
</script>La regola p { color: blue } colora solo il paragrafo all'interno di questa radice — un <p> altrove nella pagina non viene toccato. La regola :host (descritta di seguito) stilizza l'elemento host stesso.
Selezionare l'host: :host, :host(), :host-context()
Uno shadow root non può selezionare il proprio elemento host con un selettore normale, perché l'host vive al di fuori della radice. Tre pseudo-classi colmano questa lacuna:
| Selettore | Corrisponde a | Usalo per |
|---|---|---|
:host | L'elemento host, sempre | Stili di base del componente (display, padding, box). |
:host(<selector>) | L'host solo quando corrisponde a <selector> | Varianti e stati guidati da attributi/classi/pseudo-classi, ad es. :host([disabled]), :host(:hover). |
:host-context(<selector>) | L'host quando un antenato corrisponde a <selector> | Adattarsi al contesto, ad es. :host-context(.dark-theme). |
<div class="dark-theme">
<fancy-box disabled>Boxed content</fancy-box>
</div>
<script>
class FancyBox extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' }).innerHTML = `
<style>
:host {
display: block;
padding: 12px;
border: 2px solid #007bff;
}
/* Variant: applies only when the host has [disabled] */
:host([disabled]) {
opacity: 0.5;
pointer-events: none;
}
/* Context: applies when any ancestor has .dark-theme */
:host-context(.dark-theme) {
background: #1e1e1e;
color: #fff;
}
</style>
<slot></slot>
`;
}
}
customElements.define('fancy-box', FancyBox);
</script>Poiché l'elemento host inizia con [disabled] e si trova all'interno di .dark-theme, tutte e tre le regole si applicano: viene renderizzato scuro, attenuato e non interattivo.
:host-context() ha un supporto browser limitato (nessun Firefox al momento della stesura). Preferisci una proprietà personalizzata CSS o un attributo esplicito sull'host quando hai bisogno di ampia compatibilità.
Stilizzare il contenuto slottato con ::slotted()
Il contenuto che l'utente passa al tuo componente vive nel light DOM e viene renderizzato attraverso un <slot>. Tale contenuto continua ad appartenere alla pagina, quindi gli stili propri della pagina hanno la precedenza — ma puoi comunque raggiungerlo dall'interno dello shadow root con ::slotted().
Un limite importante: ::slotted() corrisponde solo ai nodi slottati di primo livello, non ai loro discendenti. ::slotted(span) funziona; ::slotted(div span) no.
<body>
<script>
class CustomButton extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
:host {
display: inline-block;
padding: 10px 20px;
background-color: #007bff;
color: #fff;
border: none;
cursor: pointer;
}
:host(:hover) {
background-color: #0056b3;
}
button {
font-weight: bold;
border: none;
background: none;
color: inherit;
cursor: inherit;
padding: 0;
}
/* Styling slotted content */
::slotted(span) {
font-style: italic;
text-decoration: underline;
}
</style>
<button>
<slot></slot>
</button>
`;
}
}
customElements.define('custom-button', CustomButton);
</script>
<!-- Test custom-button with slotted content -->
<custom-button id="my-button">Click <span>here</span></custom-button>
</body>Qui ::slotted(span) seleziona lo <span> passato come contenuto dello slot, rendendolo corsivo e sottolineato, mentre il testo "Click" circostante rimane invariato.
Consentire alla pagina di personalizzare un componente: proprietà personalizzate CSS
L'incapsulamento è ottimo, ma può sembrare un muro: la pagina host non riesce a penetrare all'interno per ricolorare un pulsante. L'escape hatch prevista sono le proprietà personalizzate CSS (variabili) — sono l'unica cosa che attraversa il confine dello shadow. Il componente legge una variabile con var() e fornisce un valore di fallback; la pagina imposta quella variabile dall'esterno.
<style>
/* The page customizes the component from outside the boundary */
theme-button {
--btn-bg: #28a745;
--btn-bg-hover: #1e7e34;
}
</style>
<theme-button>Save</theme-button>
<script>
class ThemeButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' }).innerHTML = `
<style>
:host {
/* var(--name, fallback): fallback is used if the page sets nothing */
background: var(--btn-bg, #007bff);
color: #fff;
padding: 8px 16px;
display: inline-block;
cursor: pointer;
}
:host(:hover) {
background: var(--btn-bg-hover, #0056b3);
}
</style>
<slot></slot>
`;
}
}
customElements.define('theme-button', ThemeButton);
</script>Il pulsante viene renderizzato in verde perché la pagina ha impostato --btn-bg. Rimuovi quelle due dichiarazioni e ricade sul blu (#007bff). Questo è il modo più pulito per esporre una API di tematizzazione mantenendo privati gli elementi interni del componente.
<style> vs adoptedStyleSheets
Inserire un tag <style> nell'innerHTML di ogni istanza funziona, ma duplica il testo CSS per ogni componente nella pagina e costringe il browser a ri-analizzarlo ogni volta. Per i componenti che vengono creati molte volte, condividi un unico CSSStyleSheet già analizzato tra le radici usando adoptedStyleSheets.
<my-badge>New</my-badge>
<my-badge>Beta</my-badge>
<script>
// Parsed once, reused by every instance
const sheet = new CSSStyleSheet();
sheet.replaceSync(`
:host {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
background: #007bff;
color: #fff;
font-size: 12px;
}
`);
class MyBadge extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: 'open' });
root.adoptedStyleSheets = [sheet]; // adopt the shared sheet
root.innerHTML = `<slot></slot>`;
}
}
customElements.define('my-badge', MyBadge);
</script>Quando usare quale:
<style>all'interno della radice — il più semplice, nessun codice aggiuntivo, adatto per componenti una tantum o piccole demo.adoptedStyleSheets— preferito quando lo stesso componente appare molte volte: un foglio di stile condiviso e costruibile significa meno memoria e un'istanziazione più rapida. Puoi anche aggiornare il foglio a runtime (sheet.replaceSync(...)) e ogni radice che lo adotta riflette immediatamente la modifica.
Conclusione
Lo stile dello Shadow DOM si basa su alcune idee fondamentali: gli stili sono con scope in entrambe le direzioni, si raggiunge l'host con :host / :host() / :host-context(), si raggiunge il contenuto proiettato con ::slotted(), si espone la tematizzazione tramite proprietà personalizzate CSS e si allega il CSS inline con <style> oppure in modo efficiente con adoptedStyleSheets. Insieme, consentono di distribuire componenti che appaiono correttamente ovunque, pur rimanendo personalizzabili secondo le proprie condizioni.
Per approfondire, consulta Shadow DOM Slots & Composition per capire come il contenuto slottato viene assemblato, e Web Components per combinare shadow root con elementi personalizzati e template.