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.