Back

Estilizando Web Components con Shadow DOM y CSS

Estilizando Web Components con Shadow DOM y CSS

Si alguna vez has colocado un Web Component en una página y te has preguntado por qué tu CSS global dejó de funcionar dentro de él, te has topado con la frontera del shadow. No es un error — es precisamente el propósito. El Shadow DOM otorga a los elementos personalizados su propio árbol DOM con alcance aislado, lo que significa que los estilos no se filtran ni hacia adentro ni hacia afuera por defecto. Pero eso no significa que sea imposible aplicar estilos. Significa que aplicar estilos es intencional.

Este artículo cubre los principales mecanismos para estilizar el Shadow DOM: estilos internos, :host, ::slotted(), propiedades personalizadas de CSS, ::part() y adoptedStyleSheets.

Puntos clave

  • El Shadow DOM aísla los estilos por diseño, por lo que el CSS global no puede alcanzar el interior de un componente y los estilos internos no pueden filtrarse hacia afuera.
  • Los estilos del shadow actúan como valores por defecto y se sitúan por debajo de los estilos definidos en la página dentro de la cascada, de forma similar a la hoja de estilos user-agent del navegador.
  • Usa :host y :host() para estilizar el elemento del componente, y ::slotted() para estilizar los hijos del light DOM proyectados (solo descendientes directos).
  • Las propiedades personalizadas de CSS y ::part() conforman una API pública de estilizado deliberada: variables para tokens de temas, partes para estilizado estructural.
  • adoptedStyleSheets permite que varios componentes compartan un único CSSStyleSheet ya parseado, mejorando el rendimiento a escala.

Por qué el Shadow DOM cambia el comportamiento del CSS

Cuando llamas a attachShadow() sobre un elemento, creas un árbol DOM independiente adjunto a ese elemento (el shadow host). Los selectores de la hoja de estilos de tu página no pueden alcanzar el interior de ese árbol, y los selectores dentro del shadow tree no pueden alcanzar el exterior.

Hay un matiz importante que conviene entender: los estilos del Shadow DOM se sitúan por debajo de tus estilos definidos en la cascada CSS. Eso significa que una regla global como * { color: red } sobrescribirá una regla :host { color: green } dentro del shadow — incluso si la regla global aparece antes en el orden del código fuente. Piensa en los estilos del shadow como los estilos por defecto del componente, de manera similar a cómo la hoja de estilos user-agent del navegador provee valores por defecto para <button> o <input>.

Estilizando el Shadow Host con :host

Desde dentro del shadow tree, :host selecciona el elemento al que está adjunto el shadow:

:host {
  display: block;
  font-family: sans-serif;
}

También puedes usar :host(selector) para aplicar estilos condicionalmente según los atributos o clases del host:

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

Como el elemento host vive en el light DOM, los estilos a nivel de documento sobrescribirán las reglas :host. Si quieres que los valores por defecto del componente prevalezcan, recurre a !important dentro del shadow — uno de los pocos casos en los que su uso es genuinamente apropiado.

Estilizando contenido proyectado con ::slotted()

Los slots permiten proyectar contenido del light DOM dentro de un shadow tree. El pseudo-elemento ::slotted() permite estilizar esos elementos proyectados desde dentro del shadow:

::slotted(p) {
  margin: 0;
  color: #333;
}

Limitación importante: ::slotted() solo coincide con el elemento asignado directamente a un slot — no con sus descendientes. Un selector como ::slotted(p span) no funcionará, y ::slotted() solo acepta un selector compuesto (sin combinadores de descendencia). Para un estilizado más profundo, apóyate en la herencia de CSS o deja que los propios estilos del light DOM se encarguen.

Las propiedades personalizadas de CSS cruzan la frontera del shadow

Las variables CSS (propiedades personalizadas) atraviesan libremente la frontera del shadow. Esto las convierte en la herramienta más flexible para el theming de Web Components:

/* En el shadow tree */
:host {
  background: var(--card-bg, white);
  color: var(--card-color, black);
}
/* En la hoja de estilos de tu página */
my-card {
  --card-bg: #1a1a2e;
  --card-color: #eee;
}

El componente define los puntos de enganche; el consumidor establece los valores. Incluye siempre valores de respaldo para que el componente funcione sin ninguna configuración externa.

Exponiendo puntos de estilizado con CSS Shadow Parts (::part())

CSS Shadow Parts son la respuesta moderna para estilizar elementos internos de un Web Component desde fuera. Dentro del componente, marcas los elementos con el atributo part:

<button part="trigger">Open</button>

Fuera del componente, los consumidores pueden apuntar directamente a esa parte usando ::part():

my-dialog::part(trigger) {
  background: royalblue;
  border-radius: 4px;
}

Esta es una API de estilizado intencional — el autor del componente decide qué se expone. Es más potente que las variables CSS para el estilizado estructural (layout, bordes, fondos) a la vez que mantiene los detalles de implementación interna privados.

Compartiendo estilos eficientemente con adoptedStyleSheets

adoptedStyleSheets te permite adjuntar objetos CSSStyleSheet directamente a un shadow root. Tiene un amplio soporte en los navegadores modernos y es muy adecuado para bibliotecas de componentes que necesitan compartir una hoja de estilos parseada entre muchas instancias:

const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host { display: block; }`);

class MyComponent extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.adoptedStyleSheets = [sheet];
  }
}

customElements.define('my-component', MyComponent);

El navegador parsea la hoja de estilos una sola vez, sin importar cuántos componentes la usen — una ganancia de rendimiento significativa a escala. Puedes añadir o eliminar hojas de estilos mutando el array adoptedStyleSheets, y los cambios en un CSSStyleSheet compartido se aplican en todos los lugares donde está adoptado.

Eligiendo el enfoque adecuado

ObjetivoHerramienta
Estilizar el elemento host del componente:host / :host()
Estilizar contenido proyectado del light DOM::slotted()
Exponer tokens de theming a los consumidoresPropiedades personalizadas de CSS
Exponer estilizado estructural a consumidores::part()
Compartir estilos entre muchos componentesadoptedStyleSheets

Conclusión

Estilizar con Shadow DOM no se trata de bloquear el CSS — se trata de hacer explícita la encapsulación. Usa estilos internos y :host para los valores por defecto, variables CSS y ::part() como tu API pública de estilizado, y adoptedStyleSheets cuando el rendimiento a escala importa. Una vez que tengas ese modelo mental, estilizar Web Components se vuelve sencillo.

Preguntas frecuentes

Porque el componente usa Shadow DOM, que crea un árbol DOM con alcance aislado del documento principal. Las hojas de estilos de la página no pueden seleccionar elementos dentro del shadow tree. Para estilizar el interior, el autor del componente debe exponer puntos de enganche como propiedades personalizadas de CSS o CSS Shadow Parts mediante el atributo part, a los que luego apuntas con ::part() desde fuera.

Usa propiedades personalizadas para tokens de diseño como colores, espaciados y tipografías que fluyen naturalmente a través de la herencia. Usa ::part() cuando los consumidores necesiten control estructural sobre un elemento interno específico, como sobrescribir bordes, fondos o el layout de un botón o encabezado. Las partes ofrecen un acceso más granular, mientras que las variables se mantienen más simples y de alcance más amplio.

El pseudo-elemento ::slotted() solo coincide con los nodos de nivel superior asignados directamente a un slot, no con sus descendientes. Además, solo acepta un selector compuesto, por lo que los combinadores de descendencia no son válidos. Para estilizar los hijos de los elementos proyectados, apóyate en la herencia de CSS desde el elemento proyectado o deja que la propia hoja de estilos del light DOM del consumidor maneje esos descendientes directamente.

Sí. Está soportado en todos los navegadores modernos evergreen y es la forma recomendada de compartir estilos entre muchos shadow roots sin re-parsearlos. Un único CSSStyleSheet parseado puede adjuntarse a múltiples shadow roots, lo que reduce el uso de memoria y mejora el tiempo de arranque. Las hojas de estilos compartidas también se pueden actualizar y reutilizar eficientemente entre múltiples componentes.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay