Este post fue originalmente publicado en Octuweb en octubre de 2018.
Para mucha gente CSS es cosa de magos, incluso habrá quien crea que es brujería, pero eso es por que no conocen los hechizos necesarios para tener un auténtico control sobre el CSS.
CSS Houdini es como el libro mágico que nos lleva al siguiente nivel en nuestro control de la magia, una serie de nuevos hechicos para controlar las diferentes fases en el proceso de renderizado.
.CSS
Empecemos por conocer el escenario actual.
En el diagrama podemos ver el proceso de renderizado que hace el navegador, convirtiendo el html en los pixels que acaban representados en el dispositivo.
Una vez empieza el proceso no tenemos ningún control, en ninguna de las fases, sobre cómo el navegador analizar el HTML y CSS y lo convierte en el modelo de objetos DOM y CSSOM respectivamente.
Si queremos modificar el comportamiento de alguna propiedad CSS, ya sea para corregir un bug o poder adaptar el comportamiento a nuestro contenido y diseño, lo tenemos que hacer con Javascript una vez tenemos el DOM construido, seleccionar el elemento para aplicar las nuevas propiedades CSS y generar un nuevo CSSOM.
En este otro diagrama podemos ver que con el Javascript podemos modificar el DOM y/o CSSOM, disparando de nuevo el proceso de Cascada, Layout, Paint y Composite. Esto representa un coste adicional al navegador, y aunque nos parezca que es un coste totalmente asumible en nuestra flameante máquina para desarrollar, hemos de tener en cuenta que el mayor consumo de contenidos proviene de los dispositivos móviles, donde debemos prestar especial atención a minimizar el impacto en el rendimiento.
.CSS-Houdini
The objective of the CSS-TAG Houdini Task Force (CSS Houdini) is to jointly develop features that explain the “magic” of Styling and Layout on the web. - CSS Houdini
CSS Houdini es una coleción de nuevas APIs que nos permite un acceso (parcial) al motor CSS del navegador.
Aquí podemos ver cómo cambia el tema, proponiendo una API para acceder a cada una de las fases del proceso de renderizado (los recuadros de color gris indican especificaciones planeadas, pero aún no hay nada definido).
Veamos qué APIs tenemos “disponibles” actualmente.
- Paint API: esta API nos ofrece un canvas (limitado) para dibujar en las propiedades
background
yborder
. - Typed OM API: se encarga de los valores basada en objetos para trabajar con valores CSS en JavaScript.
- Properties & Values API: proporciona una API para definir propiedades de CSS y darles un tipo, comportamiento y valor predeterminado. Muy útil para utilizar junto a otras APIs de Houdini.
- Animation Worklet: llega con la propuesta de mejorar la experiencia de usiario al separar del hilo principal las interaciones relacionadas con el scroll y el mouse. Así que tendremos animaciones, transiciones e interacciones más suaves.
- Layout API: Construye tus propios diseños, reinventa flexbox y grid e implementa todo con propiedades CSS.
- Parser API: expone el analizador CSS, esto nos permite analizar lenguajes arbitrarios similares a CSS en una representación ligeramente tipada.
- Font Metrics API: expone algunos datos de fuente en una forma compacta que le da mucho más control sobre el texto en su aplicación.
Esto nos permite extender CSS mediante Javascript.
.Extendiendo-CSS
La mejor manera de entender el potencial que tiene es con ejemplos, así que vamos a empezar a ver cómo trabajar con algunas de las nuevas APIs.
Para los ejemplos vamos a utilizar Google Chrome Canary y el flag Experimental Web Platform features activado (chrome://flags/#enableexperimental-web-platform-features), ya que es el navegador con más soporte de las nuevas APIs, como veremos más adelante.
& .Worklets
Un worklet es básicamente lo que vamos a utilizar para conectar con el motor de CSS del navegador. Se trata de un módulo en Javascript donde definiremos la “magia” que podremos usar en CSS. No tenemos control sobre la ejecución de los worklets, el motor de renderizado es quien hace las llamadas cuando es necesario.
El punto más interesante de un worlet es que no tiene impacto en el rendimiento, porque cada worklet funciona dentro de su propio hilo de ejecución. La potencia de Javascript con la suabidad de CSS.
Para añadir un módulo worklet lo haremos con el método addModule
.
<script>
if ("paintWorklet" in CSS) {
CSS.paintWorklet.addModule("PlaceholderBoxPainter.js");
}
</script>
En este caso comprobamos que disponemos de la Paint API con el condicional if ('paintWorklet' in CSS)
, esto nos permitiría poder presentar una alternativa y no cargar el módulo si no está soportado.
Este sería el contenido del módulo PlaceholderBoxPainter.js
class PlaceholderBoxPainter {
paint(ctx, size) {
// Magic 🎩
}
}
registerPaint("placeholder-box", PlaceholderBoxPainter);
No sufras si no entiendes algo, con el ejemplo de la Paint API encajarán las piezas.
& .Paint-API
Esta ha sido la primera API en llegar a la versión estable de Chrome (versión 65, Marzo 2018).
En el punto aterior hemos visto cómo cargar un módulo worlet, ahora vamos a ver un ejemplo de Paint API.
Lo primero que vamos ha hacer es cargar nuestro worklet, si está soportado por el navegador.
<script>
if ("paintWorklet" in CSS) {
CSS.paintWorklet.addModule("PlaceholderBoxPainter.js");
} else {
document.querySelector("html").classList.add("no-support");
// Aquí añadirmos una clase al <html> para mostrar un mensage
// o la alternativa como fallback
}
</script>
<div class="placeholder"></div>
Este es el contenido de nuestro módulo PlaceholderBoxPainter.
class PlaceholderBoxPainter {
paint(ctx, size) {
ctx.lineWidth = 2;
ctx.strokeColor = "#FC5D5F";
// Dibuja una linea desde arriba a la izquierda
// hasta abajo a la derecha.
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(size.width, size.height);
ctx.stroke();
// Dibuja una línea desde arriba a la derecha
// hasta abajo a la izquierda
ctx.beginPath();
ctx.moveTo(size.width, 0);
ctx.lineTo(0, size.height);
ctx.stroke();
}
}
registerPaint("placeholder-box", PlaceholderBoxPainter);
El primer detalle que podemos ver es que estamos definiendo una clase, y no necesitaremos un transpiler como Babel, ya que el navegador lo interpreta de forma nativa.
En el ejemplo tenemos un sólo método, el paint()
que es el que se ejecutará cada vez que el motor CSS necesite repintar el elemento. Tenemos dos parámetros iniciales, ctx
el contexto donde podremos utilizar una versión limitada del objeto CanvasRenderingContext2D
(no podremos dibujar texto o trabajar con bitmaps) y el segundo parámetro es size
donde tenemos el ancho y alto del elemento.
Como indica en los comentarios del código, nuestro nuevo worklet lo que hará es pintat una X, como se suele utilizar como placeholder en los mockups para hacer referencia a una imagen.
Hemos definido ctx.lineWidth = 2
y ctx.strokeColor = '#FC5D5F'
como valores por defecto para el grosor y color.
Por último, con registerPaint('placeholder-box', PlaceholderBoxPainter)
estamos registrando el módulo, donde pasamos como primer parámetro el nombre que utilizaremos en el CSS y el nombre de la clase que acabamos de definir.
Veamos como queda el CSS.
.placeholder {
background-image: paint(placeholder-box);
...;
}
Aquí es donde vemos una nueva función, paint(placeholder-box)
con el parámetro que hemos utilizado en el registro del módulo.
Como cualquier propiedad CSS, podemos definir un fallback añadiento una líena previa.
.placeholder {
background-image: linear-gradient(to bottom, #fc5d5f 0%, #a53d3d 100%);
background-image: paint(placeholder-box);
...;
}
Aquí puedes ver el ejemplo Placeholder Box
&-CustomProperties
Vamos a hacer nuestro módulo más dinámico añadiendo Custom Properties a nuestra clase.
CSS
.placeholder {
--line-with: 2;
--stroke-color: #fc5d5f;
background-image: linear-gradient(to bottom, #fc5d5f 0%, #a53d3d 100%);
background-image: paint(placeholder-box);
...;
}
Aquí hemos añadido dos nuevas propiedades CSS y les hemos asignado un valor. Habrás visto que no son propiedades que reconozcas, no te preocupes en un momento hablaremos de ellas.
Worklet
class PlaceholderBoxPainter {
static get inputProperties() {
return ["--line-with", "--stroke-color"];
}
paint(ctx, size) {
ctx.lineWidth = properties.get("--line-with");
ctx.strokeColor = properties.get("--stroke-color");
// Dibuja una linea desde arriba a la izquierda
// hasta abajo a la derecha.
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(size.width, size.height);
ctx.stroke();
// Dibuja una línea desde arriba a la derecha
// hasta abajo a la izquierda
ctx.beginPath();
ctx.moveTo(size.width, 0);
ctx.lineTo(0, size.height);
ctx.stroke();
}
}
registerPaint("placeholder-box", PlaceholderBoxPainter);
Como habrás visto, hemos añadido varias líneas de códido a nuestro módulo worklet.
Por un lado hemos añadido un método estático para obtener las propiedades que tengamos definidas en el contexto de la declaración CSS. Aquí definimos las propiedades que queremos utilizar en nuestro worklet, en este caso --line-with
y --stroke-color
.
static get inputProperties() {
return [
'--line-with',
'--stroke-color'
];
}
También hemos cambiado la asignación de grosor y color de la línea.
ctx.lineWidth = properties.get("--line-with");
ctx.strokeColor = properties.get("--stroke-color");
JS
CSS.registerProperty({
name: "--line-with",
syntax: "<number>",
inherits: false,
initialValue: 1,
});
CSS.registerProperty({
name: "--stroke-color",
syntax: "<color>",
inherits: true,
initialValue: "rebeccapurple",
});
Aquí entra en escena otra de las nuevas APIs, la Properties & Values API (operativa en la versión Canari de Google Chrome). Con la que podemos registrar nuestras propias propiedades CSS. Creo que esta API es brutal, analicemos el objeto que le estamos pasando como argumento.
name
con el que definimos el nombre de la propiedad CSS.syntax
nos permite definir el “tipo” de dato aceptado, en caso de que no sea el definido lo ignora.inherits
con un valor boleano indicamos si admite herencia del parent o no.initialValue
el valor inicial de la propiedad.
Al definir estas propiedades, las podemos utilizar como si de variables se trataran para tenerlas disponibles en nuestro módulo worklet.
Puedes ver los cambios aquí
.Soporte
Sé que esta parte es la que desanima a mucha gente, pero como cualquier avance en los desarrollos de la web tenemos un período de implementación en los navegadores. Surma mantiene la página Is Houdini ready yet‽, donde presenta una tabla donde podemos ver el soporte de las APIs en los diferentes navegadores.
Sí, aún está todo muy verde, bueno muy rojo 😅.
La única API con Candidate Recommendation en la W3C es la Paint API, que ya está disponible en las versiones estables de Chrome y Opera, como también lo está la API de Type OM, aun con un estado de Working Draft en la W3C. Activado los flags experimentales en Canary, podremos empezar a jugar con Layout API, Properties & Values API y Animation Worklet.
.Conclusión
He leído varios artículos definiendo a CSS Houdini como la solución para desarrollar polyfills para CSS o como el Babel para CSS (cosa que creo que es un efoque totalmente erróneo).
Es posible que te estés preguntando, como seguro lo hace Naiara, Diana o Ángel 😉, si podremos definir comportamientos personalizados que no están definidos en la especificación de la W3C, ¿eso no se está alejando de la filosofía de los estándares web?
IMHO, creo que con esta propuesta, nos están dando acceso a unas nuevas APIs que nos ofrecen más “poder” y control. Y como toda nueva tecnología tendrá un período de aceptación donde veremos ejemplos de una implementación no muy consensuada… pero de momento, *disfrutemos de la opción de mejorar nuestra magia con estos nuevos hechizos.
.Referencias
Os dejo una lista de recursos donde encontraréis enlaces a documentación, artículos, vídeos y lo más importante: ejemplos donde poder analizar el comportamiento (recordad utilizar Chrome Canary con el chrome://flags/#enableexperimental- web-platform-features
activado).