Variables CSS y CSS Parts

Formas no intrusivas de cambiar estilos

Como habremos comprobado en artículos anteriores, hay muchas formas de añadir CSS en un componente y cada una tiene sus ventajas y desventajas particulares. Sin embargo, existen algunos trucos considerables para permitir cambiar los estilos en un WebComponent de una forma no intrusiva.

Variables CSS

Probablemente, la forma más interesante que tengan los componentes de cambiar los estilos de forma cómoda y práctica, que muchas veces no se le presta la debida atención es a las Variables CSS (Custom Properties). Se pueden definir desde nuestro documento global, permiten definir valores fallback y tienen concepto de ámbito en el DOM, por lo que son un sistema muy potente y flexible al ahora de utilizarlas.

:root {
--gradient: linear-gradient(indigo, blueviolet);
}

.element {
background: var(--gradient, purple);
}

.warning {
--gradient: red;
background: var(--gradient, black);
}

Observa que en el ejemplo anterior, hemos definido una variable --gradient que contiene una función para crear un gradiente lineal con CSS.

En principio, la hemos definido en :root, por lo que existe para todo el documento, puesto que esta pseudoclase es equivalente a <html>. Sin embargo, en .warning la hemos redefinido, por lo que sólo para ese elemento la custom property --gradient valdrá red, mientras que tendrá el valor del gradiente para el resto de la página HTML.

Aprender más sobre Variables CSS (Custom Properties)

Filtrado a través de Shadow DOM

Pero quizás, en el contexto de WebComponents, uno de los detalles más importantes y más desconocido es que las Variables CSS son capaces de penetrar y filtrarse a través del Shadow DOM de un elemento, por lo que son una estrategia perfecta para organizar nuestros estilos con ellas.

Veamos un ejemplo donde utilizaremos CSS Custom Properties para dar estilo a un componente. Observa que tenemos 3 componentes y en el CSS global, hemos seleccionado el primero de ellos y le hemos establecido la variable --bgcolor a color naranja:

<app-element></app-element>
<app-element></app-element>
<app-element></app-element>

<style>
/* CSS Global */
app-element:first-of-type {
--bgcolor: orangered;
}
</style>

Así pues, el primer componente tiene la variable --bgcolor establecida a naranja, mientras que en el resto la variable --bgcolor no está definida. Veamos ahora la implementación del componente:

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

connectedCallback() {
this.shadowRoot.innerHTML = /* html */`
<style>
.element {
display: inline-block;
padding: 6px 20px;
background: var(--bgcolor, grey);
color: white;
}
span {
font-weight: bold;
vertical-align: super;
font-size: small;
color: gold;
}
</style>
<div class="element">
AppElement <span>New!</span>
</div>
`
;
}
};
customElements.define("app-element", AppElement);

Como se puede ver, en el interior del componente definimos background: var(--bgcolor, grey), lo que significa que usará el color de fondo establecido en la variable --bgcolor, y si esa variable no está definida, utilizará un color gris. Con esto conseguimos preparar nuestro componente para tener valores por defecto que se modificarán dependiendo de lo que indique el usuario que consume el componente, estableciendo valores seguros de fallback en el caso de que no estén indicados.

Recuerda que puedes utilizar la pseudoclase :host() para establecer las variables en el componente dependiendo del contenedor padre del elemento. Puedes encontrar más información sobre esto en CSS Scoping en Shadow DOM.

CSS Parts

En el contexto de componentes, también tenemos un mecanismo para organizar y simplificar la forma en la que creamos partes de un componente que queremos que se le de estilo desde el exterior. La idea clave de este sistema, es identificar y exponer las partes de un componente, utilizando el atributo HTML part con el nombre que deseemos.

Veamos como quedaría el connectedCallback() del ejemplo anterior, creando varias partes:

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

connectedCallback() {
this.shadowRoot.innerHTML = /* html */`
<style>
:host {
display: inline-block;
padding: 6px 20px;
background: var(--bgcolor, grey);
color: white;
}
</style>
<div class="element">
<span part="content">AppElement</span>
<span part="badge">New!</span>
</div>
`
;
}
};
customElements.define("app-element", AppElement);

Como se puede ver, tenemos un componente con fondo gris muy similar al de ejemplo anteriores. La diferencia es que hemos identificado dos partes y las hemos expuesto al exterior:

  • Una parte content para el contenido del componente.
  • Una parte badge para las decoraciones del componente.

Ahora, desde un enfoque de código CSS global y desde fuera del componente, podríamos utilizar el pseudoelemento CSS ::part(name) para indicar la parte concreta de un componente a las que queremos dar estilo:

app-element {
--bgcolor: steelblue;
}
app-element::part(content) {
font-size: 1.25rem;
font-weight: bold;
padding: 15px;
}
app-element::part(badge) {
font-weight: bold;
vertical-align: super;
font-size: small;
color: gold;
background: black;
padding: 10px;
}

Observa que no sólo puedes dar estilo directamente a partes expuestas del componente, sino que también puedes filtrar variables CSS en contenedores o en las propias partes, como hemos aprendido en temas anteriores.

Además, esta especificación también brinda una API Javascript, para poder manejar las partes desde la lógica de nuestro componente, permitiendo añadir, eliminar, reemplazar o conmutar a través de métodos como .add(), remove(), replace() o toggle(), o comprobar si existe mediante .contains(), similar a como se hace con la API classList de Javascript.

Tabla de contenidos