With the new CSS Paint API (aka Houdini, presumably named after the Melvins album 🤘😁), we can use most of the HTML Canvas drawing methods to draw an image or shape and use it in any CSS property that takes an image. Today I want to show how I used Houdini in my newly-rebuilt portfolio site* to generate border images and speech-bubble-shaped divs. I will also cover using the polyfill, using Houdini with webpack and Babel, and the snags I hit while making the following demos.
*Not currently polyfilled - you will see a fallback unless you view it with Chrome
The basics of using Houdini are as follows: for any CSS property that takes an image, such as background-image, enter paint(workletName)
as the value. In a JS file, create an ES6 class for your worklet. In the same file, call the registerPaint
method with the workletName
and the class name as the arguments. Then, in your main JS file or webpack entry point, feature detect for CSS.paintWorklet
. If it's there, which right now is only in Chrome, call CSS.paintWorklet.addModule('./myWorkletClassFile.js')
; otherwise, after npm i -S css-paint-polyfill
, we can dynamically import the polyfill so it will be a separate webpack chunk, then call addModule
. Now we are ready to develop our class and generate some art! The repo for this article:
jamessouth / paint-demo
repo for my first article on the css paint api
paint-demo
Demos from my article on the new CSS Paint (Houdini) API.
Demo 3 (Chrome only)
Generating a page's background
So let's start with generating a page's background. Demo 1 is live here.
Normally I wouldn't have a <style>
tag in the HTML, but there is some kind of caching issue when paint
declarations are made in SCSS, at least for worklets that run on page load. To see what I mean, check out Demo 4 in Chrome - the page fails to load every other time you hit reload 😢.
What I believe to be the same caching issue also causes problems for me on Firefox - without dev tools open and Disable Cache checked, the polyfill almost never runs 😭. This could be due to the way I have my demo repo set up, or the hosting on GitHub. The polyfill is pretty reliable on Edge and Safari in my testing.
You will see the background-color on the body when the polyfill doesn't run, not the gradient fallback. The polyfill works by creating an image, so if you resize or re-orient, you will get repeats or cut-offs of the original image formed when the page loaded. Repeats can be prevented with background-repeat
set to no-repeat
; you will also just see the background-color on the body. Since Chrome has some native support for Houdini, when you resize or re-orient, the worklet runs again and redraws to fit the new dimensions, so watch out for that if you write a complex paint function.
The fake placeholder content in Demo 1 is in a div which will hold the painted background and cover the page. This is a workaround for this bug in Chrome which breaks CSS custom properties set on the body
(also apparently html
and :root
), at least with regard to accessing them in a paint worklet. The remaining CSS is :
A second workaround for the Chrome bug is to use pseudo-content on the body, but then the polyfill doesn't work. A third workaround is to set custom properties on the body anyway, then in the worklet, test for the presence of props. If they're not there (as will be the case with Chrome), set a default value. Anyway, let's get to the worklet!
In our worklet class we can create static helper methods for use in the paint
method, where we do our drawing. The paint
method takes 1 to 4 arguments:
- the canvas context (
ctx
) on which you call the drawing methods - the dimensions of the element you are drawing on, which we can just destructure as
{ width, height }
-
props
, which gives you access to CSS custom properties - an
args
array that holds arguments passed in when you call the paint worklet from CSS, likepaint(workletName, arg1, arg2, ...)
As of right now there is no support anywhere for args 😭. Watch Is Houdini ready yet? for updates.
Now we are ready to build! As far as I can tell, the Worklet interface only accepts ES6 classes, so a transpiled-to-ES5-function worklet doesn't work, and neither does a class wrapped in a function by webpack (if there's a way to just minify in webpack please answer my question on Stack Overflow). So, I have been processing them outside of webpack. This works fine but makes iterating in development a little slower. Install the babel-minify
package as a dev dependency, then in package.json
minify your worklet files and place them in your /dist
folder:
In my webpack config, I use the CleanWebpackPlugin
and delete everything except the minified worklet files:
To develop the worklet I then move it to the /dist
folder and name it demo1.min.js
since that is the name I'm using elsewhere. Now when I start webpack-dev-server
, /dist
is wiped except for the worklet and the development workflow is normal except for having to manually refresh the browser to reflect a change to the worklet. When I'm done, I move the worklet back to source (renaming to demo1.js
) and build for production. The prebuild script will minify the worklet and webpack will take care of the rest!
Generating border images
Demo 2 (live site) has a similar structure to Demo 1, just some dummy content:
It is styled similarly to Demo 1 except we are using Houdini to generate border images:
Just drawing a bunch of lines that form the image we set as the border image source:
In my portfolio site, I only used this technique for focus styles and only for a bottom border; here are the SCSS and worklet.
Generating arbitrarily-shaped elements
We can use Houdini to carve any shape out of a div with the mask-image
property (here is my portfolio's speech bubble worklet). Any element we do this to will still occupy a rectangle in the CSS box model of course, but within its box we can achieve any look we want. For this third demo, I went a little crazy: I re-created the POP! explosion lithograph that American artist Roy Lichtenstein made for the cover of the April 25, 1966, issue of Newsweek. This one only works in Chrome because the polyfill does not seem to like multiple paint
values in a single style sheet, but you can use multiple style sheets with one paint
invocation each to work around this issue. Demo 3 is live here.
Remaining CSS for the cloud and outline:
As far as the worklet code goes, there is little difference between masking and drawing. For the exclamation point and cloud outline, either way works since they are solid colors. The red/yellow/white explosion is solid too but I drew it so that I could apply the dark outlines; it does not seem possible to both mask a shape and have an outline around it, which is why the cloud's outline is a separate worklet.
I tried to pattern the blue cloud in the worklet but it wasn't looking good. A nested loop can be used to draw across the width and height of the subject element, but I didn't find a way to keep what is drawn confined within the cloud's boundaries. I also tried the ctx.createPattern() method but I couldn't find a way to get an image into the worklet (no DOM access) to be the pattern source. So, I made the cloud's pattern in CSS then masked the shape in a worklet, thus requiring another worklet to make the outline.
To help draw these shapes I used this tool which generates the draw instructions and adds x- and y-offsets, which I then used to position the shape within the div.
More on the polyfill and conclusion
One last thing I wanted to show with the polyfill was that calling paint
in your CSS before other declarations seems to work better than putting it just anywhere. I made Demo 4 to show the aforementioned caching issue when paint
is called in SCSS, but also to say that if other declarations come before it the polyfill doesn't seem to run as often, so you just get the body's background color. The cover image of this article is a shot of Demo 4.
Before I discovered this placement made a difference, it took seemingly random declarations like display: block
(even though that is already the default on a div) to get the polyfill to run 😖😕😵🤦♂️.
Houdini is nascent technology with growing browser support, but we can already do lots of cool things with our backgrounds, borders, and divs. If you can imagine it, you can draw it! I hope you found this article helpful and are inspired and empowered to use paint
to push your front-end creativity forward. Please leave a comment and share widely! Thank you!
Top comments (0)