Estilos en el Shadow DOM
Aplica estilos a web components encapsulados con Shadow DOM: :host, :host(), :host-context(), ::slotted(), propiedades personalizadas CSS y adoptedStyleSheets.
El Shadow DOM proporciona a un componente su propio árbol DOM privado y sus propios estilos privados. Esta página se centra en la parte de los estilos: cómo el CSS escrito dentro de un shadow root queda encapsulado, los selectores especiales que se usan para acceder al host y al contenido distribuido (:host, :host(), :host-context(), ::slotted()), cómo permitir que el CSS externo personalice un componente de forma intencionada (propiedades personalizadas) y las dos formas de adjuntar una hoja de estilos (<style> vs adoptedStyleSheets).
Si el Shadow DOM es nuevo para ti, lee primero JavaScript Shadow DOM y consulta Web Components para entender el panorama general de dónde encajan los shadow roots.
Por qué los estilos están encapsulados
La promesa fundamental del Shadow DOM es una barrera de estilos bidireccional:
- El CSS externo no se filtra hacia adentro. Una regla
p { color: red }a nivel de página no afectará a un<p>dentro de un shadow root. Esto es lo que hace que los componentes sean seguros para insertar en cualquier página. - El CSS interno no se filtra hacia afuera. Los estilos de un shadow root solo se aplican dentro de ese root, por lo que puedes usar selectores cortos y genéricos (
button,p,.title) sin preocuparte por conflictos con la página anfitriona.
Esto es diferente del modelo habitual de estilos y clases, donde todos los selectores compiten en un único ámbito global. Dentro de un shadow root, el ámbito es el comportamiento predeterminado.
Creación de un shadow root
Para empezar, adjunta un shadow root a un elemento host. Todo lo que pongas dentro de él — marcado y CSS — quedará encapsulado.
<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>Aquí adjuntamos un shadow root con el método attachShadow() y establecemos su mode en 'open', lo que permite leer el root posteriormente mediante element.shadowRoot. ('closed' lo oculta a los scripts externos, pero no añade seguridad real.)
Añadir estilos con ámbito mediante <style>
La forma más sencilla de aplicar estilos a un shadow root es colocar un elemento <style> dentro de él. Esas reglas solo se aplican dentro del root, y las reglas de la página quedan excluidas.
<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 regla p { color: blue } solo colorea el párrafo dentro de este root — un <p> en cualquier otra parte de la página no se ve afectado. La regla :host (más abajo) aplica estilos al propio elemento host.
Apuntando al host: :host, :host(), :host-context()
Un shadow root no puede seleccionar su elemento host con un selector normal, porque el host vive fuera del root. Tres pseudoclases salvan esa distancia:
| Selector | Coincide con | Úsalo para |
|---|---|---|
:host | El elemento host, siempre | Estilos base del componente (display, padding, caja). |
:host(<selector>) | El host solo cuando coincide con <selector> | Variantes y estados basados en atributos/clases/pseudoclases, p. ej. :host([disabled]), :host(:hover). |
:host-context(<selector>) | El host cuando un ancestro coincide con <selector> | Adaptarse al contexto, p. ej. :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>Dado que el elemento host comienza con [disabled] y se encuentra dentro de .dark-theme, las tres reglas se aplican: se renderiza oscuro, atenuado y no interactivo.
:host-context() tiene soporte limitado en los navegadores (sin Firefox en el momento de escribir esto). Prefiere una propiedad personalizada CSS o un atributo explícito en el host cuando necesites amplia compatibilidad.
Aplicando estilos al contenido distribuido con ::slotted()
El contenido que el usuario pasa a tu componente vive en el light DOM y se renderiza a través de un <slot>. Ese contenido sigue perteneciendo a la página, por lo que los estilos propios de la página prevalecen — pero aun así puedes acceder a él desde dentro del shadow root con ::slotted().
Un límite importante: ::slotted() solo coincide con los nodos distribuidos de nivel superior, no con sus descendientes. ::slotted(span) funciona; ::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>Aquí ::slotted(span) apunta al <span> pasado como contenido del slot, aplicándole cursiva y subrayado, mientras que el texto "Click" circundante se deja intacto.
Permitir que la página personalice un componente: propiedades personalizadas CSS
La encapsulación es excelente, pero puede sentirse como una barrera: la página anfitriona no puede acceder al interior para cambiar el color de un botón. La vía de escape prevista son las propiedades personalizadas CSS (variables) — son lo único que sí traspasa la barrera del shadow. El componente lee una variable con var() y proporciona un valor de reserva; la página establece esa variable desde el exterior.
<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>El botón se renderiza en verde porque la página estableció --btn-bg. Elimina esas dos declaraciones y vuelve al azul de reserva (#007bff). Esta es la forma más limpia de exponer una API de temas manteniendo privados los internos del componente.
<style> vs adoptedStyleSheets
Poner una etiqueta <style> en el innerHTML de cada instancia funciona, pero duplica el texto CSS para cada componente en la página y obliga al navegador a volver a analizarlo cada vez. Para componentes que se crean muchas veces, comparte una única CSSStyleSheet analizada entre los roots con 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>Cuándo usar cada uno:
<style>dentro del root — lo más simple, sin código adicional, adecuado para componentes de un solo uso o demos pequeños.adoptedStyleSheets— preferible cuando el mismo componente aparece muchas veces: una hoja de estilos construible compartida significa menos memoria e instanciación más rápida. También puedes actualizar la hoja en tiempo de ejecución (sheet.replaceSync(...)) y cada root que la adopte reflejará el cambio instantáneamente.
Conclusión
Los estilos en el Shadow DOM se basan en algunas ideas clave: los estilos tienen ámbito en ambas direcciones, accedes al host con :host / :host() / :host-context(), accedes al contenido proyectado con ::slotted(), expones los temas mediante propiedades personalizadas CSS y adjuntas el CSS bien de forma inline con <style> o de forma eficiente con adoptedStyleSheets. Todo ello te permite distribuir componentes que se ven bien en cualquier lugar y que siguen siendo personalizables según tus condiciones.
Para profundizar más, consulta Shadow DOM Slots & Composition para ver cómo se ensambla el contenido distribuido, y Web Components para combinar shadow roots con elementos personalizados y plantillas.