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.
Analicemos las formas de dar estilo a nuestros WebComponents.
Formas tradicionales
En el primer grupo, veremos tres formas tradicionales de aplicar CSS a nuestros WebComponents.
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, repasemos que nos ofrece esta solución:
- ✅ HTML escrito dentro del componente (HTML modular)
- ✅ Funcionalidad escrita dentro del componente (JS modular)
- 💔 CSS escrito de forma global (CSS no es modular)
- 💔 El CSS externo al componente le afecta (no está aislado)
- 💔 El CSS interno del componente afecta al exterior (no está aislado)
Este sistema puede servirnos para ejemplos pequeños, pero es difícil de mantener si crece.
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 ciertos detalles que podrían ser considerados desventajas en algunos escenarios:
- ✅ HTML escrito dentro del componente (HTML es modular)
- ✅ Funcionalidad escrita dentro del componente (JS es modular)
- ✅ CSS escrito dentro del componente (CSS es modular)
- 💔 El CSS externo al componente le afecta (CSS no aislado)
- 💔 El CSS interno del componente afecta al exterior (CSS no 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.
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 (HTML es modular)
- ✅ Funcionalidad escrita dentro del componente (JS es modular)
- ✅ CSS escrito en un fichero a parte del componente (CSS es modular)
- 💔 El CSS externo al componente le afecta (CSS no aislado)
- 💔 El CSS interno del componente afecta al exterior (CSS no aislado)
- 💔 El navegador hace peticiones extra 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.
Formas modernas
Analicemos ahora algunas formas de dar estilo a nuestros WebComponents mediante mecanismos más modernos y nuevos.
Una forma particularmente interesante y moderna, es utilizar la regla @scope
. Esta regla de CSS nos permite crear estilos que están limitados a la parte del DOM donde la escribes (o incluso limitaciones más complejas):
class AppElement extends HTMLElement {
constructor() {
super();
this.innerHTML = /* html */`
<style>
@scope {
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>
Como puedes ver, los estilos están dentro de una regla @scope
que limita la aplicación del CSS a los elementos de ese componente, por lo tanto, no aplica fuera de ellos.
Repasemos las ventajas de este enfoque:
- ✅ HTML escrito dentro del componente (HTML es modular)
- ✅ Funcionalidad escrita dentro del componente (JS es modular)
- ✅ CSS escrito dentro del componente (CSS es modular)
- 💔 El CSS externo al componente le afecta (CSS no aislado)
- ✅ El CSS interno del componente no afecta al exterior (CSS aislado)
Esto puede ser interesante en algunas situaciones donde queremos el CSS global afecte al componente (por ejemplo, usando librerías o frameworks de CSS), pero no queremos que el CSS del componente afecte fuera del componente.
Aquí puedes leer más información sobre la regla
@scope
de CSS.
Una última opción sería importar ficheros .css
obteniendo un objeto Constructable 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. Se suele conocer como Módulos CSS (no confundir con el «CSS modules» usado en frameworks de Javascript).
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" with { 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 fragmentos de código puedes encontrarte
assert
en lugar dewith
. La palabra claveassert
era la que se utilizaba antiguamente, pero está marcada como obsoleta y la que se utilizará eswith
.
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 (HTML es modular)
- ✅ Funcionalidad escrita dentro del componente (JS es modular)
- ✅ CSS escrito en un fichero a parte del componente (CSS es modular)
- 💔 El CSS externo al componente le afecta (CSS no aislado)
- 💔 El CSS interno del componente afecta al exterior (CSS no aislado)
- ✅ El CSS se evalua de forma estática, no dinámica (mejor rendimiento)
- 💔 El soporte de import
with
es aún irregular (compatibilidad)
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 (HTML es modular)
- ✅ Funcionalidad escrita dentro del componente (JS es modular)
- ✅ CSS escrito dentro del componente (CSS es modular)
- ✅ El CSS externo al componente no le afecta (CSS aislado)
- ✅ El CSS interno del componente no afecta al exterior (CSS aislado)
- ✅ Tenemos mecanismos nativos de control de CSS para Shadow DOM
- 💔 La curva de aprendizaje de Shadow DOM con Javascript suele ser dura
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 (HTML es modular)
- ✅ Funcionalidad escrita dentro del componente (JS es modular)
- ✅ CSS escrito dentro del componente (CSS es modular)
- ✅ El CSS externo al componente no le afecta (CSS aislado)
- ✅ El CSS interno del componente no afecta al exterior (CSS aislado)
- 💔 Librería externa (no nativa), coste de descarga/parseo (peor rendimiento)
- 💔 El CSS se evalua de forma dinámica, añadiendo clases CSS (peor 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... Cada una con diferentes enfoques.