CSS en WebComponents

Estilos en un WebComponent con Shadow DOM


Más atrás, vimos las diferentes formas de añadir CSS a nuestros Custom Elements. Sin embargo, el gran problema que teníamos entonces es que el CSS afectaba tanto al componente como al exterior del componente, ya que CSS tiene una naturaleza global por defecto. De la misma forma, el CSS exterior afectaba al componente.

CSS en WebComponents

Ahora que ya sabemos crear Shadow DOM para aislar nuestro componente, convendría conocer las diferentes formas que tenemos a nuestra disposición para manejar los estilos CSS en un WebComponent. Es importante que el lector conozca ¿Qué es el Shadow DOM?, ¿Qué es el Light DOM? y otros conceptos de WebComponents antes de continuar, ya que trabajaremos con ellos.

Estilos CSS en un WebComponent

Vamos a crear un componente que contendrá un Shadow DOM para aislar su contenido e impedir tanto que el CSS exterior acceda dentro del componente, como que el CSS del componente afecte a elementos fuera del componente. Para ello, empezaremos creando un archivo en /components/AppElement.js:

<script src="./components/AppElement.js"></script>

<h2>Titular global</h2>
<app-element></app-element>

En su interior, crearemos un componente aislado con Shadow DOM. Observa que en el constructor() del componente hemos utilizado .attachShadow() para crear un Shadow DOM en modo abierto:

class AppElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = /* html */`
      <div class="container">
        <h2>Títular del componente</h2>
        <p>Texto y descripción del contenido del componente.</p>
      </div>
    `;
  }
}

customElements.define("app-element", AppElement);

Recuerda también, que a la hora de hacer el .innerHTML se debe hacer sobre el .shadowRoot para acceder al Shadow DOM del componente y no al DOM principal del documento. Solo haciendo esto, conseguiremos que nuestro componente esté aislado, como comprobaremos a continuación.

Mediante CSS global

Si desde nuestro index.css global intentamos darle estilo a los <h2> que encuentre, siguiendo el enfoque global tradicional de CSS, nos encontraremos con lo siguiente:

h2 {
  color: red;
}

Comprobaremos que este h2 sólo cambiará los elementos <h2> que estén en el documento global o en componentes sin Shadow DOM, pero no modificará los elementos con Shadow DOM, ya que están aislados del exterior.

app-element h2 {
  color: red;
}

Aún realizando este intento, tampoco lograremos modificarlos. Los estilos del componente están aislados. Por lo tanto, este enfoque no nos interesa por varias razones:

  • ✅ HTML escrito dentro del componente (modular)
  • ✅ Funcionalidad escrita dentro del componente (modular)
  • ❌ CSS escrito dentro del componente (modular)
  • ✅ CSS fuera del componente, no afecta al componente

Pero claro, este enfoque global tampoco nos sirve ya que ahora no podemos cambiar los estilos del componente desde fuera.

Mediante un bloque de estilos

La estrategia preferida en WebComponents suele ser utilizar la etiqueta <style> en el marcado HTML del componente. De esta forma podremos escribir los estilos CSS que afectarán a toda la página en el index.css (global) y los estilos que afectan sólo al componente en el bloque <style> de dicho componente con Shadow DOM.

Veamos un ejemplo de como se vería esta modalidad:

class AppElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = /* html */`
      <style>
        h2 { color: indigo; }
        p { color: blueviolet; }
      </style>
      <div class="container">
        <h2>Títular del componente</h2>
        <p>Texto y descripción del contenido del componente.</p>
      </div>
    `;
  }
}

customElements.define("app-element", AppElement);

En este caso, podremos comprobar que los estilos incluidos en la etiqueta <style> sólo afectarán al HTML del Shadow DOM del componente, por lo que estará perfectamente aislado y no afectarán al resto del documento.

  • ✅ HTML escrito dentro del componente (modular)
  • ✅ Funcionalidad escrita dentro del componente (modular)
  • ✅ CSS escrito dentro del componente (modular)
  • ✅ CSS fuera del componente, no afecta al componente
  • ✅ CSS del componente, no afecta al exterior del componente

Además, el hecho de tener completamente aislado el componente, hace posible la opción de no necesitar metodologías como BEM para evitar colisiones de clases, y simplemente utilizar elementos HTML muy básicos a la hora de dar estilos y cuando nuestro componente se comience a complicar, separar en nuevos componentes.

Mediante estilos externos

Hay desarrolladores que no ven con buenos ojos el uso de las etiquetas <style> o el hecho de añadir marcado dentro de template strings. Esto, aunque muchas veces suele ser costumbre o hábito de otros frameworks Javascript, en muchas ocasiones existe una necesidad de separar el componente en ficheros separados por tecnologías.

Aunque este enfoque no es recomendable en navegadores (si no se usa transpilación), se puede conseguir utilizando etiquetas <link> o regla CSS @import. De esta forma, podemos acceder a ficheros CSS que estén fuera de nuestro componente .js:

<app-element></app-element>
class AppElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = /* html */`
      <style>
        @import "./components/AppElement.css";
      </style>
      <div class="container">
        <h2>Títular del componente</h2>
        <p>Texto y descripción del contenido del componente.</p>
      </div>
    `;
  }
};

customElements.define("app-element", AppElement);

Nota: Ten en cuenta que en este caso, la ruta del archivo .css debe partir desde donde está el documento HTML principal, y no desde donde está el fichero del componente, por lo que cuidado al establecer rutas relativas.

Esta mecánica sería equivalente a añadir una etiqueta <style> con una regla @import donde estaríamos también haciendo una petición a un archivo .css externo desde el navegador:

<style>
  @import "./components/AppElement.css";
</style>

En este caso, tenemos algunas desventajas sobre el punto anterior:

  • ✅ HTML escrito dentro del componente (modular)
  • ✅ Funcionalidad escrita dentro del componente (modular)
  • ✅ CSS escrito en un fichero .css separado del componente (modular)
  • ✅ CSS fuera del componente, no afecta al componente
  • ✅ CSS del componente, no afecta al exterior del componente
  • ❌ El navegador cargará el fichero del componente, pero estará haciendo una petición extra al fichero .css.
  • ❌ Debido a lo anterior, es posible que haya cierta latencia o retardo en la parte visual mientras carga los estilos.

Mediante CSS construible

Por último, existe una forma más de añadir los estilos desde un fichero externo e incorporarlo a nuestro componente, los CSS Constructables. Se trata de realizar una importación de un fichero .css a un objeto Javascript, que tendrá toda la información del archivo pero en un formato de objeto JS, manipulable y modificable a través de Javascript:

import styles from "./AppElement.css" with { type: "css" };

class AppElement extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }

  connectedCallback() {
    this.shadowRoot.adoptedStyleSheets.push(styles);
    this.shadowRoot.innerHTML = /* html */`
      <div class="container">
        <h2>Títular del componente</h2>
        <p>Texto y descripción del contenido del componente.</p>
      </div>
    `;
  }
}

customElements.define("app-element", AppElement);

Recuerda mucho al enfoque que se sigue en los diferentes frameworks Javascript, salvo con la diferencia que eliminamos herramientas intermediarias para hacer estas tareas (Node, Webpack, etc...), lo hace directamente el navegador, y permite cosas más potentes, al tener una API propia.

Posteriormente, podríamos utilizar la propiedad .adoptedStyleSheets del Shadow DOM para incluir ese objeto CSSStyleSheet que acabamos de importar en el componente. Se puede utilizar cualquiera de las dos formas siguientes:

this.shadowRoot.adoptedStyleSheets = [...document.adoptedStyleSheets, styles];
this.shadowRoot.adoptedStyleSheets.push(styles);

Observa que la forma «programática» de adoptar los estilos importados es añadir al array .adoptedStyleSheets la desestructuración de los estilos adoptados actuales en el documento, más el CSS que acabamos de importar.

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev