Skip to content

CSS Houdini, More Than Magic

Published:

This post was originally published on Octuweb in October 2018.

For many people CSS is a magician’s thing, some might even believe it’s witchcraft, but that’s because they don’t know the necessary spells to have real control over CSS.

CSS Houdini is like the magic book that takes us to the next level in our magic control, a series of new spells to control the different phases in the rendering process.

.CSS

Let’s start by understanding the current scenario.

Rendering progress

In the diagram we can see the rendering process the browser does, converting HTML into the pixels that end up represented on the device.

Once the process starts we have no control, in any of the phases, over how the browser parses HTML and CSS and converts it into the DOM and CSSOM object models respectively.

If we want to modify the behavior of any CSS property, whether to fix a bug or to adapt the behavior to our content and design, we have to do it with JavaScript once we have the DOM built, select the element to apply the new CSS properties and generate a new CSSOM.

Rendering process polyfilled

In this other diagram we can see that with JavaScript we can modify the DOM and/or CSSOM, triggering again the Cascade, Layout, Paint and Composite process. This represents an additional cost to the browser, and although it may seem like a totally acceptable cost on our fancy development machine, we must keep in mind that most content consumption comes from mobile devices, where we should pay special attention to minimizing performance impact.

.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 is a collection of new APIs that gives us (partial) access to the browser’s CSS engine.

CSS Houdini rendering process

Here we can see how things change, proposing an API to access each of the phases of the rendering process (the gray boxes indicate planned specifications, but nothing is defined yet).

Let’s see what APIs we have “available” currently.

This allows us to extend CSS through JavaScript.

.Extending-CSS

The best way to understand the potential is with examples, so let’s start seeing how to work with some of the new APIs.

For the examples we’re going to use Google Chrome Canary and the Experimental Web Platform features flag activated (chrome://flags/#enable-experimental-web-platform-features), as it’s the browser with the most support for the new APIs, as we’ll see later.

& .Worklets

A worklet is basically what we’re going to use to connect with the browser’s CSS engine. It’s a JavaScript module where we’ll define the “magic” we can use in CSS. We have no control over worklet execution, the rendering engine is what makes the calls when necessary.

The most interesting point of a worklet is that it has no performance impact, because each worklet runs within its own execution thread. The power of JavaScript with the smoothness of CSS.

Worklets

To add a worklet module we’ll use the addModule method.

<script>
  if ("paintWorklet" in CSS) {
    CSS.paintWorklet.addModule("PlaceholderBoxPainter.js");
  }
</script>

In this case we check that we have the Paint API available with the conditional if ('paintWorklet' in CSS), this would allow us to present an alternative and not load the module if it’s not supported.

This would be the content of the PlaceholderBoxPainter.js module

class PlaceholderBoxPainter {
  paint(ctx, size) {
    // Magic 🎩
  }
}

registerPaint("placeholder-box", PlaceholderBoxPainter);

Don’t worry if you don’t understand something, with the Paint API example the pieces will fit together.

& .Paint-API

This has been the first API to reach the stable version of Chrome (version 65, March 2018).

In the previous point we saw how to load a worklet module, now let’s see an example of the Paint API.

The first thing we’re going to do is load our worklet, if it’s supported by the browser.

<script>
  if ("paintWorklet" in CSS) {
    CSS.paintWorklet.addModule("PlaceholderBoxPainter.js");
  } else {
    document.querySelector("html").classList.add("no-support");
    // Here we add a class to <html> to show a message
    // or the alternative as fallback
  }
</script>

<div class="placeholder"></div>

This is the content of our PlaceholderBoxPainter module.

class PlaceholderBoxPainter {
  paint(ctx, size) {
    ctx.lineWidth = 2;
    ctx.strokeColor = "#FC5D5F";

    // Draw a line from top left
    // to bottom right.
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(size.width, size.height);
    ctx.stroke();

    // Draw a line from top right
    // to bottom left
    ctx.beginPath();
    ctx.moveTo(size.width, 0);
    ctx.lineTo(0, size.height);
    ctx.stroke();
  }
}

registerPaint("placeholder-box", PlaceholderBoxPainter);

The first detail we can see is that we’re defining a class, and we won’t need a transpiler like Babel, since the browser interprets it natively.

In the example we have a single method, paint() which is what will be executed every time the CSS engine needs to repaint the element. We have two initial parameters, ctx the context where we can use a limited version of the CanvasRenderingContext2D object (we can’t draw text or work with bitmaps) and the second parameter is size where we have the element’s width and height.

As indicated in the code comments, our new worklet will paint an X, as is usually used as a placeholder in mockups to reference an image.

We’ve defined ctx.lineWidth = 2 and ctx.strokeColor = '#FC5D5F' as default values for thickness and color.

Finally, with registerPaint('placeholder-box', PlaceholderBoxPainter) we’re registering the module, where we pass as first parameter the name we’ll use in CSS and the name of the class we just defined.

Let’s see how the CSS looks.

.placeholder {
  background-image: paint(placeholder-box);
  ...;
}

This is where we see a new function, paint(placeholder-box) with the parameter we used in the module registration.

Like any CSS property, we can define a fallback by adding a previous line.

.placeholder {
  background-image: linear-gradient(to bottom, #fc5d5f 0%, #a53d3d 100%);
  background-image: paint(placeholder-box);
  ...;
}

Here you can see the example Placeholder Box

&-CustomProperties

Let’s make our module more dynamic by adding Custom Properties to our class.

CSS
.placeholder {
  --line-with: 2;
  --stroke-color: #fc5d5f;
  background-image: linear-gradient(to bottom, #fc5d5f 0%, #a53d3d 100%);
  background-image: paint(placeholder-box);
  ...;
}

Here we’ve added two new CSS properties and assigned them a value. You’ll have seen that they’re not properties you recognize, don’t worry we’ll talk about them in a moment.

Worklet
class PlaceholderBoxPainter {
  static get inputProperties() {
    return ["--line-with", "--stroke-color"];
  }

  paint(ctx, size, properties) {
    ctx.lineWidth = properties.get("--line-with");
    ctx.strokeColor = properties.get("--stroke-color");

    // Draw a line from top left
    // to bottom right.
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(size.width, size.height);
    ctx.stroke();

    // Draw a line from top right
    // to bottom left
    ctx.beginPath();
    ctx.moveTo(size.width, 0);
    ctx.lineTo(0, size.height);
    ctx.stroke();
  }
}

registerPaint("placeholder-box", PlaceholderBoxPainter);

As you’ll have seen, we’ve added several lines of code to our worklet module.

On one hand we’ve added a static method to get the properties we have defined in the CSS declaration context. Here we define the properties we want to use in our worklet, in this case --line-with and --stroke-color.

static get inputProperties() {
  return [
    '--line-with',
    '--stroke-color'
  ];
}

We’ve also changed the line thickness and color assignment.

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",
});

Here another of the new APIs comes into play, the Properties & Values API (operational in the Canary version of Google Chrome). With which we can register our own CSS properties. I think this API is amazing, let’s analyze the object we’re passing as argument.

By defining these properties, we can use them as if they were variables to have them available in our worklet module.

You can see the changes here

.Support

I know this part is what discourages many people, but like any advance in web development we have an implementation period in browsers. Surma maintains the page Is Houdini ready yet‽, where he presents a table where we can see API support in different browsers.

Is Houdini ready yet‽

Yes, everything is still very green, well very red 😅.

The only API with Candidate Recommendation at the W3C is the Paint API, which is already available in stable versions of Chrome and Opera, as is the Type OM API, even with a Working Draft status at the W3C. Activating experimental flags in Canary, we can start playing with Layout API, Properties & Values API and Animation Worklet.

.Conclusion

I’ve read several articles defining CSS Houdini as the solution for developing polyfills for CSS or as the Babel for CSS (which I think is a totally wrong approach).

You might be wondering, as Naiara, Diana or Ángel surely do 😉, if we’ll be able to define custom behaviors that aren’t defined in the W3C specification, isn’t that moving away from the philosophy of web standards?

IMHO, I think with this proposal, they’re giving us access to new APIs that offer us more “power” and control. And like any new technology it will have an acceptance period where we’ll see examples of not very consensual implementation… but for now, let’s enjoy the option of improving our magic with these new spells.

.References

I’ll leave you a list of resources where you’ll find links to documentation, articles, videos and most importantly: examples where you can analyze behavior (remember to use Chrome Canary with chrome://flags/#enable-experimental-web-platform-features activated).


Previous Post
CSS Pixel Art
Next Post
Motion Craftsmanship