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 las diferentes formas que existen de dar estilo a nuestro componente.

Formas nativas de escribir CSS

Como hemos mencionado anteriormente, cuando creamos un componente lo que buscamos es simplificar y evitar repetir código, por lo que nuestro componente debe ofrecernos una forma flexible y potente de añadir estilos CSS para ese componente en cuestión.

Observa este ejemplo de base, donde tenemos un custom element o webcomponent muy básico, con un código HTML básico en su interior. En los siguientes apartado, añadiremos estilo al componente de varias formas diferentes, para comprender cuales son las formas que podemos utilizar:

class AppElement extends HTMLElement {
  constructor() {
    super();
    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);
<h2>Titular global</h2>
<app-element></app-element>

De momento es un componente sin estilos CSS. Observa que el Titular global está en el HTML, fuera del componente.

Tenemos 4 formas principales de dar estilo al componente:

  • 1️⃣ Mediante CSS global
  • 2️⃣ Mediante CSS en un string
  • 3️⃣ Mediante CSS externo
  • 4️⃣ Mediante CSS construíble

Analicemos las características de cada una de ellas.

Mediante CSS global

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

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 o nomenclaturas CSS 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 {
  background: steelblue;
  color: white;
}
class AppElement extends HTMLElement {
  constructor() {
    super();
    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);
<h2>Titular global</h2>
<app-element></app-element>

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)
  • 💔 El CSS externo al componente no le afecta (aislado)
  • 💔 El CSS interno del componente no afecta al exterior (aislado)

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

Mediante CSS en un string

Vamos a intentar solucionar la desventaja anterior de no tener el código CSS en el interior del componente, usando un bloque de estilos <style> y obteniendo el código CSS desde un .

Esta es la forma más extendida actualmente de incluir estilos en un WebComponent, ya que es sencilla y no requiere peticiones adicionales:

class AppElement extends HTMLElement {
  constructor() {
    super();
    this.innerHTML = /* html */`
      <style>
        h2 {
          background: hotpink;
          color: white;
        }
      </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);
<h2>Titular global</h2>
<app-element></app-element>

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)
  • 💔 El CSS externo al componente no le afecta (aislado)
  • 💔 El CSS interno del componente no afecta al exterior (aislado)

A pesar de tener los estilos CSS escritos dentro del componente, la naturaleza del CSS sigue siendo global, por lo que esos estilos sobre el elemento <h2> no se aplicarán únicamente al titular <h2> del componente como quizás pensemos, sino que se va a aplicar a todos los elementos <h2> de la página, dentro y fuera del componente.

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

Mediante CSS externo

Otra opción que se nos podría ocurrir es utilizar una etiqueta <link> o un @import en un bloque <style> 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 {
  constructor() {
    super();
    this.innerHTML = /* html */`
      <link rel="stylesheet" href="./AppElement.cdn.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);
<h2>Titular global</h2>
<app-element></app-element>

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)
  • 💔 El CSS externo al componente no le afecta (aislado)
  • 💔 El CSS interno del componente no afecta al exterior (aislado)
  • 💔 El navegador hace peticiones adicionales para descargar 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 desde el navegador, tendrá que hacerse una petición extra en cliente, provocando una cierta latencia que, aunque es despreciable, puede afectar al rendimiento visual.

Además, hay que tener en cuenta que las rutas se evaluan en cliente (en el navegador), por lo que habría que ser cuidadoso al indicar las rutas y asegurarse que son las rutas finales.

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.cdn.css" assert { type: "css" };

class AppElement extends HTMLElement {
  constructor() {
    super();
    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);
<h2>Titular global</h2>
<app-element></app-element>

En el código del componente he utilizado assert ya que es la palabra clave que tiene soportado Chrome en la actualidad. En el futuro será reemplazada por with, pero no tiene soporte actualmente.

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)
  • 💔 El CSS externo al componente no le afecta (aislado)
  • 💔 El CSS interno del componente no afecta al exterior (aislado)
  • ✅ El CSS se evalua de forma estática, no dinámica. (rendimiento)
  • 💔 El soporte de import with / assert es irregular (compatibilidad)

Mediante CSS con Shadow DOM

Todas las modalidades anteriores tienen la misma desventaja: los estilos del componente afectan al resto del documento y viceversa. Más adelante aprenderemos a crear estilos CSS utilizando Shadow DOM en nuestros componentes, una forma nativa de solucionar dichas desventajas.

  • ✅ HTML escrito dentro del componente (modular)
  • ✅ Funcionalidad escrita dentro del componente (modular)
  • ✅ CSS escrito dentro del componente (modular)
  • ✅ El CSS externo al componente no le afecta (aislado)
  • ✅ El CSS interno del componente no afecta al exterior (aislado)
  • ✅ Tenemos mecanismos nativos de control de CSS para Shadow DOM
  • 💔 La curva de aprendizaje de Shadow DOM suele ser dura inicialmente

Aprender más sobre Shadow DOM

Formas no nativas de escribir CSS

En los frameworks Javascript, la forma que la comunidad ha escogido para solucionar la desventaja de que los estilos de un componente afecten al resto del documento y viceversa, es modificar las clases CSS mediante una librería Javascript. Este tipo de librerías se encuentran dentro de una categoría llamada CSS-in-JS.

Estas formas se pueden utilizar también en los WebComponents. Veamos un ejemplo, utilizando la librería CSS-in-JS Emotion:

import { css } from "https://cdn.jsdelivr.net/npm/@emotion/[email protected]/+esm";

const styles = css`
  h2 {
    background: green;
    color: white;
  }
`;

class AppElement extends HTMLElement {
  constructor() {
    super();
    this.classList.add(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);
<h2>Titular global</h2>
<app-element></app-element>

Observa que hemos escrito el código CSS en un objeto style que utiliza el helper css de Emotion. Este devolverá una clase CSS autogenerada, como css-ck3uk3, que añadiremos al componente mediante this.classList.add(styles).

En este caso, repasemos las ventajas/desventajas:

  • ✅ HTML escrito dentro del componente (modular)
  • ✅ Funcionalidad escrita dentro del componente (modular)
  • ✅ CSS escrito dentro del componente (modular)
  • ✅ El CSS externo al componente no le afecta (aislado)
  • ✅ El CSS interno del componente no afecta al exterior (aislado)
  • 💔 Librería externa (no nativa), coste de descarga/parseo (rendimiento)
  • 💔 El CSS se evalua de forma dinámica, añadiendo clases CSS (rendimiento)

Existen muchas otras librerías CSS-in-JS similares a Emotion que son agnósticas, y no requieren frameworks, como ecsstatic, linaria, Goober, vanilla-extract u otros.

¿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