Exploring the New CSS Houdini Painting API

Low-level APIs that hook into the styling and layout process of your browser’s rendering engine

Gerard van der Put
Jan 7 · 7 min read
Chart showing if Houdini is ready for different browsers
Chart showing if Houdini is ready for different browsers
Source: ishoudinireadyyet.com

Note: The source code for the demo discussed in the second half of this article can be found on GitLab.

Intro

As I mentioned in my recent article about canvas animation inside React components, I love HTML canvas. So I couldn’t be more excited to learn about the new CSS Houdini APIs when I was reading a short article about it on css-tricks.com, by Stephen Fulghum.

The main reason for my excitement is that the Painting API makes it possible to create custom CSS images by drawing onto a PaintRenderingContext2D (which is pretty much an exact copy of the 2D context that we draw onto when we use the regular Canvas API — except for a small subset of features).

By using the Painting API we can programmatically draw images and use those images in our CSS. When drawing, we can receive parameters with information coming from the DOM and applied stylesheets.

The MDN Web Docs describe it like this:

Houdini is a set of low-level APIs that exposes parts of the CSS engine, giving developers the power to extend CSS by hooking into the styling and layout process of a browser’s rendering engine.

I find it very exciting, especially since we see signs that all major browsers are implementing this:

We will have a look at the CSS Painting API (and Worklets) in this article.

Important note: CSS Houdini is still an experimental technology, in general. But as mentioned before, most browsers are implementing it or are strongly considering implementing it. Google Chrome is an early adaptor and ships support for the Painting API since version 65, so that’s the browser we will be using today.

CSS Painting API

By using this API we can programmatically draw images and use those images in our CSS. This is what we will create:

Lorem ipsum text
Lorem ipsum text

Impressively enough, these are three DIV elements and three DIV elements only.

Our index.html file is evidence for that:

The “styling” (drawing…) for the background of the three panes is done with the Painting API, programmatically. All three panes are drawn by the same function.

Let’s have a look at the CSS class pane in our stylesheet:

We paint something called pane, on line 2. That requires explanation, and we will have a look at it soon.

It might be important to note that the paint function is not executed only once. It’s executed — and will re-paint the image — whenever the render engine of the browser gives it instructions to do so. Examples of that happening are when the user resizes the browser window, or when other CSS properties of the DIV element change, and the DIV element gets another dimension.

Since our function will be “hooked into” the render engine, this is a very performant and low-cost operation.

Other than that it’s worth mentioning that all our panes have a different font size (keep this in mind, we will refer to it later on again). And we see two custom CSS variables being used:

  • ‑‑dot‑spacing
  • ‑‑pane‑color

CSS variables are nothing new. They have been around since 2014/2016 (resp. Firefox/Google Chrome). But one of the new CSS Houdini APIs called CSS properties and values API allows us to register these custom variables so the browser will know more about them, which will come in handy.

Inside our stylesheet we can register them like this:

Note that we could also have done this in JavaScript. The result will be the same.

So why do we do this? Now our browser, and its render engine, know the details about these properties. It knows that ‑‑pane‑color contains a color value and that the default value is “#646464”. And ‑‑dot‑spacing contains a length value, defaulting to “5px.”

Our two new variables are now called registered custom variables.

Back to this line:

background-image: paint(pane);

The Painting API allows us to paint an image. The paint function receives one parameter. This parameter is a JavaScript class that’s registered as a Paint in a file called worklet.js:

But! This code cannot be executed in our regular JavaScript execution environment. It needs to be executed inside a so-called — hence the filename — Worklet:

“The Worklet interface is a lightweight version of Web Workers and gives developers access to low-level parts of the rendering pipeline. With Worklets, you can run JavaScript and WebAssembly code to do graphics rendering or audio processing where high performance is required.”

We can make sure the Worklet will be executed by adding the following line to our regular JavaScript file (main.js), which is loaded inside index.html, like this:

// main.js
CSS.paintWorklet.addModule("worklet.js");

If that call returns an error, your browser does not support the Painting API.

Now our Paint class “Pane” is registered under the name “pane,” and we can use it in our CSS as we saw earlier:

background-image: paint(pane);

Details of the Paint class Pane

Let’s go over the details of the Paint class inside worklet.js:

The static function inputProperties should return a list of CSS properties that we’re interested in when we will draw our image. This is arbitrary, and you can add any CSS property you want.

The value returned by contextOptions indicates that we want to be able to use transparency in our canvas.

And finally, on line 10, the paint function. In that function, we will do the actual drawing. In our case, it receives three parameters:

  1. ctx: the 2D context of our canvas. This should ring a bell if you’re familiar with the regular HTML Canvas element.
  2. size: a PaintSize instance with two properties: .width and .height. Those are the computed dimensions of the HTML element that we’re drawing an image for. Including the padding, if set.
  3. styleMap: a read-only representation of a CSS-declaration block (source). It’s an instance of a StylePropertyMapReadOnly and only contains values for the properties we defined inside the static function inputProperties.

For more details about the last parameter, you can read about the CSS Typed Object Model API and/or read about the StylePropertyMapReadOnly interface.

Tip: You can get a complete StylePropertyMapReadOnly (containing all the computed CSS styles) for any HTML element in the DOM by calling computedStyleMap, in browsers that support it:

const styleMap =
document.getElementById('myElement').computedStyleMap();

To clarify how we can retrieve values from such an instance, I created a snippet with some inline comments:

Painting the Actual Image

Back to the logic of our little demo. The only thing left is the content of the body of our paint function.

We could start by simply drawing a filled rectangle as our background-image (you should recognize the commands from drawing on a regular HTML canvas):

…which would look like this:

Drawn background image in Houdini
Drawn background image in Houdini
Drawn background images!

Notice how we used our custom CSS property ‑‑pane‑color to dynamically set the fill style.

By using the dev-tools of our browser we can even update the color value, and the background-image will be re-painted instantly!

Google Chrome dev-tools, color picker.
Google Chrome dev-tools, color picker.
Google Chrome dev-tools, color picker.

If we update our paint function with some more fancy logic (you can look at the details in the worklet.js file in the repository), our background-image will look like this:

Lorem ipsum text
Lorem ipsum text

CSS Transitions

Notice the dot sequence in the bottom-right corner. The spacing between those dots is determined by the value of the custom CSS property ‑‑dot‑spacing, which has a default value of “5px.”

Let’s have a bit of fun by adding a transition to this property. We also increase the value of the dot-spacing when we hover over our .pane element:

…which results in this smooth animation when hovering our elements:

Animated image showing hovering
Animated image showing hovering

It illustrates how performant our custom paint function is and that it’s very capable of running 60+ times per second.

The reason we’re able to add a transition to a custom CSS variable is that we registered the property earlier on (see lines 1–5 in the gist).

Tip: We could also add an infinite CSS animation for one of our custom CSS properties which would lead to us having a custom drawn animated background!

Conclusion

I find the Houdini Painting API not only interesting but also easy to understand and powerful (performant). My mind is full of ideas for using this new functionality, but I have to be patient and wait until all major browsers support it before we can consider using this in production.

Thanks for your time!

Better Programming

Advice for programmers.

Sign up for The Best of Better Programming

By Better Programming

A weekly newsletter sent every Friday with the best articles we published that week. Code tutorials, advice, career opportunities, and more! Take a look

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Gerard van der Put

Written by

Full-stack lead developer for a large industry-leading tech company and enjoys writing about development in his spare time.

Better Programming

Advice for programmers.

Gerard van der Put

Written by

Full-stack lead developer for a large industry-leading tech company and enjoys writing about development in his spare time.

Better Programming

Advice for programmers.