CSS en Custom Elements

Estilos en un WebComponent sin Shadow DOM


—— ——

Hasta ahora hemos visto como crear un componente, añadir código HTML y dotarlo de cierta funcionalidad. Sin embargo, no hemos visto como podemos dar estilo a nuestro componente. Existen múltiples formas de hacerlo. Veamos cada una de ellas y entendamos su funcionamiento y posibles problemáticas.

Estilos CSS en un Custom Element

Vamos a crear un nuevo ejemplo donde crearemos un custom element muy básico, que añadirá un cierto marcado HTML desde su interior. Simplemente, se trata de un elemento que contiene un titular <h2> y un párrafo de texto. También colocaremos un <h2> fuera del componente, en el documento general, para las pruebas con CSS que haremos más adelante:

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

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

En el interior del componente ./components/AppElement.js tenemos el siguiente fragmento de código donde creamos el Custom Element:

class AppElement extends HTMLElement {
  connectedCallback() {
    this.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);

De momento es un componente sin estilos CSS. Veamos ahora las formas que tendríamos de dar estilo a este componente.

Mediante CSS global

Si quisieramos dar estilo a estos elementos del componente, simplemente podríamos utilizar el enfoque tradicional de CSS y hacerlo de forma global, en el fichero index.css general del documento:

h2 {
  color: red;
}

Sin embargo, con esto tendríamos un problema ya conocido en el mundo de CSS: le estamos dando estilo a todos los elementos <h2> del documento y no sólo al de nuestro componente, que probablemente sea nuestro objetivo (aunque no tiene por qué serlo). Para evitarlo, podríamos añadir una clase específica, o utilizar metodologías como BEM o similares.

Por otro lado, también podríamos, simplemente añadir el nombre del componente <app-element> antes del h2, de modo que sólo le dará estilo si está dentro de un componente <app-element>:

app-element h2 {
  color: steelblue;
}

Perfecto, hemos solucionado el problema. Sin embargo, la solución tiene algunas desventajas:

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

Este sistema puede servirnos para ejemplos pequeños, pero es difícil de mantener si crece.

Mediante un bloque de estilos

Vamos a intentar solucionar la desventaja anterior de no tener el código CSS encapsulado incluyendo dicho código CSS en el interior del componente, a través de un bloque de estilos <style>. Esta es la forma más extendida actualmente de incluir estilos en un WebComponent:

class AppElement extends HTMLElement {
  connectedCallback() {
    this.innerHTML = /* html */`
      <style>
        h2 {
          color: hotpink;
        }
      </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 ejemplo, hemos solucionado un problema. Ahora mismo, tenemos los estilos escritos dentro del componente, por lo que tenemos todo modularizado en el fichero del componente. Sin embargo, nuevamente tenemos desventajas:

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

A pesar de tener los estilos CSS escritos dentro del componente, la naturaleza de CSS es global, por lo que esos estilos sobre el elemento <h2> no se aplicarán únicamente al titular <h2> del componente como quizás cabría esperar, sino que le van a dar estilos a todos los elementos <h2> del documento.

De la misma forma, podría afectar elementos del CSS global en el componente, dependiendo de la especificidad que tenga.

Mediante estilos externos

Otra opción que se nos podría ocurrir es utilizar una etiqueta <link> que cargue un fichero .css externo. Una opción algo más similar a como funcionan los frameworks de Javascript (por que estas librerías cambian el código (transpilan) antes de que llegue al navegador).

Hablamos de esta situación:

class AppElement extends HTMLElement {
  connectedCallback() {
    this.innerHTML = /* html */`
      <link rel="stylesheet" href="./components/AppElement.css">
      <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);

Este caso sería exactamente el mismo si en lugar de la etiqueta <link>, incluimos un bloque <style> con una regla @import al fichero en cuestión. Aunque es otra forma de hacerlo, se trata del mismo caso anterior:

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

Repasemos las ventajas y desventajas que tenemos ahora:

  • ✅ HTML escrito dentro del componente (modular)
  • ✅ Funcionalidad escrita dentro del componente (modular)
  • ✅ CSS escrito en un fichero a parte del componente (modular)
  • 💔 CSS fuera del componente, afecta al componente
  • 💔 CSS del componente, afecta fuera del componente
  • 💔 Más latencia: El navegador debe cargar el componente y luego el CSS

En esta opción hemos añadido una nueva desventaja. Como el fichero .css está en un fichero diferente del componente, y estos ficheros se cargan en el navegador, el fichero CSS tendrá que descargarse y eso añadirá una cierta latencia, que aunque muchas veces es despreciable, podría afectar al rendimiento visual.

Mediante CSS construible

Una última opción serían las denominadas Constructables StyleSheet. Se trata de una forma nativa de importar ficheros .css desde el navegador y convertir su contenido a un objeto Javascript construible, que posteriormente se puede incorporar al documento.

Observa el import de la primera línea, donde importamos un fichero .css, advirtiéndole con el with { type: "css" } del tipo de fichero que es. Por último, mediante document.adoptedStyleSheets podemos adoptar el contenido CSS en el documento actual.

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

class AppElement extends HTMLElement {
  connectedCallback() {
    document.adoptedStyleSheets.push(styles);
    this.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);

Ten en cuenta que al estar utilizando un import en nuestro fichero del componente, es necesario cargarlo con una etiqueta HTML <script type="module"> para que funcionen correctamente desde el navegador.

  • ✅ HTML escrito dentro del componente (modular)
  • ✅ Funcionalidad escrita dentro del componente (modular)
  • ✅ CSS escrito en un fichero a parte del componente (modular)
  • 💔 CSS fuera del componente, afecta al componente
  • 💔 CSS del componente, afecta fuera del componente
  • ✅ El CSS se evalua de forma estática, no dinámica.

Componentes con Shadow DOM

Sin embargo, todas las opciones anteriores tienen la misma desventaja: los estilos del componente afectan al resto del documento y viceversa. En los siguientes temas aprenderemos a crear y utilizar Shadow DOM en nuestros WebComponents. Se trata de una forma de crear un DOM particular sólo para nuestro componente, que está aislado del DOM de la página general. De esta forma, podemos trabajar en el DOM particular del componente y aplicarle estilos, sin que afecte al resto del documento.

Aprender más sobre Shadow DOM

¿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