On a recent ShopTalk Video about Breakin’ up CSS Custom Properties where we discussed the different degrees of CSS Custom Property usage, I mouthblogged an idea about using CSS Houdini to help make this easier. It stuck with me so I took it out of the mouthblogosphere and made it a reality.
See the Pen Alpha Paintlet Demo by Dave Rupert (@davatron5000) on CodePen.
View Source on GitHub View Demo on CodePen
And… it works! If you’re wondering why I made this, the tl;dr is that Sass has a superior way of adjusting the alpha channel on ANY COLOR (rgb, hsl, named color, etc) but that does not work with CSS Custom Properties. This little paint worklet restores that flavor of color manipulation to backgrounds and borders. Here’s a video with a more in-depth explanation:
How it works
To use Alpha Paintlet on your site, it’s a pretty simple two step process. First, add this to your JavaScript:
CSS.paintWorklet.addModule('path/to/alpha.js')
Then in your CSS, you create a --bg-color
custom property (this can be any valid CSS color) and a --bg-alpha
custom property (from 0 to 1) and then set your CSS background
to paint(alpha)
.
#my-div {
--bg-color: #FF0000; /* rbg/hsl/etc value */
--bg-alpha: 0.23;
background: paint(alpha)
}
Behind the scenes, the paint()
function is creating a canvas image. The key feature that gets us closer to that Sass-like way of authoring is that canvas accepts ANY COLOR as a fillStyle
and we can adjust the canvas context’s globalAlpha
as well. Understanding that it makes a canvas that produces a background image, I applied paint(alpha)
to borders as well using the CSS border-image-source
.
I’m happy with the results! A minimalist approach (10 lines of code?) without exploding variables. It gets me the exact effect I mouthblogged. If you’d like to use this yourself, please check out Alpha Paintlet on GitHub.
Writing the paint worklet
The code to make the paint worklet was actually a lot less than I imagined. I thought I would be writing some low-level shader in C, but the paint worklet API is an old friend: canvas… or canvas-with-some-caveats, I should say.
registerPaint('alpha', class {
static get inputProperties() {
return ['--bg-alpha', '--bg-color']
}
paint(ctx, size, props) {
ctx.globalAlpha = props.get('--bg-alpha');
ctx.fillStyle = props.get('--bg-color');
ctx.fillRect(0, 0, size.width, size.height);
}
})
That’s it. That’s the whole paint worklet. Knowing I get a canvas context — with one giant caveat you don’t have access to the window
or the document
from a worker — lowers the barrier of entry for creating my own custom rendering to nearly zero. I’ve made dozens of experiments in canvas over the years, so this unlocks new realms of possibilities for me via the Paint API.
Progressive enhancement, future thinking, and the new relative color syntax
Support for the Paint API is not the best, 69.9% (nice) at the time of writing. Chromium only, basically. That’s too bad. You’ll probably have to progressively enhance your way into this situation. Your mileage may vary there. I hoped Houdini support would be better by now.
But invoking Houdini to use a canvas context to fade a color is kind of overkill, isn’t it? Wouldn’t it be great if you could manipulate colors in CSS?
Yes, it would! And in fact, there’s a new bit of CSS expressly for this purpose and it works today in Safari 15 TP with the Relative Color Syntax
experimental flag turned on.
#my-div {
background: rgb(from #F00 r g b / 23%)
}
This new syntax will create a new rgb
color from #F00
and destructure those three color channels to r
, g
, and b
values. We can append our alpha value after that. Adam Argyle showed me you that relative colors can take any input and convert it to any output and you can even use calc()
on the destructured value to manipulate colors inside the function.
#my-div {
background: lch(from #F00 calc(l*2) c h / 23%)
}
I wasn’t a fan of the syntax when I first saw it. I felt like I was looking at a linear-gradient or animation. But it grew on me the more I understood it as a way to apply destructuring to a color, like destructuring something in JavaScript. I could probably bikeshed the syntax, but I like the outcome so I’m willing to accept a bit of awkwardness. I’m reaching, but I sort of like how it reads from left to right, so I’ll hang on that.
Breaking out the browser support matrix…
With Safari not supporting Houdini, Chrome not supporting color functions, and Firefox supporting neither; where’s that leave the Alpha Paintlet? I don’t know, to be honest. You could do all three styling routes for awhile: a fallback, a paint function, and a relative color. CSS is forgiving in that it’ll skip the rules it doesn’t understand.
#my-div {
--bg-color: red;
--bg-alpha: 0.2;
background: var(--bg-color);
background: paint(alpha);
background: rgb(from var(--bg-color) r g b / var(--bg-alpha))
}
It’s ugly, but it works. We start to lose some of the simplicity, though. I probably won’t do this. I’ll either progressively enhance or wait around to see what lands first: Houdini Paint or CSS Relative Color Syntax.
Paraphrasing something Chris Coyier said, we’re headed into weird times with these APIs landing at different times in browsers. There’s going to be some webcompat issues and some stalemates. The minor incompatibilities are going to frustrate some folks, and websites are going to look different in different browsers, and we’ll have to explain that to people that pay us money… but when we get through it, CSS will be so powerful and wonderful in a about three years we will laugh about the old, dumb days together at a conference if those ever happen again.