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.
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.