In my previous post1, I covered how Chrome’s mask-image
support doesn’t do any antialiasing for radial-gradient
, and what that meant for making notification badge cutouts. In this post, I’ll go over a different way of providing a mask-image
that does have antialiasing (in all browsers!), namely SVGs.
Recall that we left off with a mask that looked something like this:
<style>
.take3 .pfp {
border-radius: 50%;
mask-image:
radial-gradient(
ellipse 20% 20% at 80% 20%,
transparent 100%,
white 100%
),
linear-gradient(
to bottom,
white 40%,
transparent 40%
),
linear-gradient(
to left,
transparent 20%,
white 20%
);
mask-composite: subtract;
}
</style>
<div class="horiz">
<div class="avatar take3">
<img class="pfp" src="/randomlogo.png">
</div>
<div class="avatar take3">
<div class="pfp"></div>
</div>
</div>
That is, one that was a little chunky on the curved parts. This got me wondering: is it possible to do better?
As it turns out, yes!! Using an svg instead of a CSS geometry object works a lot better. In the work codebase, I wrote a fancy SVG generator that embeds things directly inline with url('data:image/svg+xml;utf8,${svg}')
, because we already use a lot of templated CSS generated by JS, but for this demonstration I’ll just be using regular urls:
<style>
.take4 .pfp {
border-radius: 50%;
mask-image: url("/2024-03-15/mask1.svg");
/* This is used because it's a lot easier to generate complex SVGs that are black and white, instead of transparent and not transparent */
mask-mode: luminance;
}
</style>
Great!! Now, let’s just slap our notification badge back on and… voilà!
<style>
.badge {
font-variant-numeric: tabular-nums;
border-radius: 14px;
background-color: var(--color-orange);
font-size: 16px;
line-height: 16px;
position: absolute;
top: 6px;
left: 66px;
padding: 6px;
}
</style>
<div class="horiz">
<div class="avatar take4">
<img class="pfp" src="/randomlogo.png">
<div class="badge">100</div>
</div>
<div class="avatar take4">
<div class="pfp"></div>
<div class="badge">9001</div>
</div>
</div>
Now, for the sake of the argument, just like last time, let’s consider what happens if you want More Badges. And also, you want to have this be composable, so you can nest them. Well, then this happens:
<style>
.take5 {
mask-image: url("/2024-03-15/mask2.svg");
mask-mode: luminance;
}
.badge-lower {
position: absolute;
background-color: var(--color-green);
width: 28px;
height: 28px;
bottom: 6px;
right: 6px;
border-radius: 50%;
}
</style>
<div class="horiz">
<div class="avatar take4">
<div class="take5">
<img class="pfp" src="/randomlogo.png">
<div class="badge">100</div>
</div>
<div class="badge-lower"></div>
</div>
<div class="avatar take4">
<div class="take5">
<div class="pfp"></div>
<div class="badge">9001</div>
</div>
<div class="badge-lower"></div>
</div>
</div>
Gah!! Suddenly, all the talk about no-clip
and no-repeat
from the last post comes rushing back. This is no longer any sort of Chrome bug I don’t think, but rather just a way the CSS mask
property is specified to behave.
Reading the mask
specification more thoughroughly, we begin to see a solution:
- Use
no-repeat
andno-clip
(obviously) - Explicitly set the
mask-position
andmask-size
so it will only cover the part of the image we want - Combine it with another mask that does cover the whole image, using
mask-composite: exclude
to cut away at that mask.
Doing that, we come up with the following solution:
<style>
.take6 {
mask:
/* We're actually going to cut away at the lower mask, and need to use an inverted image */
/* mask-image: */ url("/2024-03-15/mask2-inverse.svg")
/* mask-mode: */ luminance
/* mask-composite: */ exclude
/* These make it so we only mask the one part */
/* mask-repeat: */ no-repeat /* mask-clip: */ no-clip
/* position the mask and size it appropriately */
/* mask-position: */ bottom left / /* mask-size: */ 100px 100px,
/* And finally, the mask that covers the whole image */
linear-gradient(white, white) no-clip luminance;
}
</style>
<div class="horiz">
<div class="avatar take4">
<div class="take6">
<img class="pfp" src="/randomlogo.png">
<div class="badge">100</div>
</div>
<div class="badge-lower"></div>
</div>
<div class="avatar take4">
<div class="take6">
<div class="pfp"></div>
<div class="badge">9001</div>
</div>
<div class="badge-lower"></div>
</div>
</div>
And there you have it! Perfectly anti-aliased, composable masks for all your CSS masking needs.
I swear we’re the only application I’ve seen that actually does something like this. Everywhere else I’ve looked either doesn’t use a mask at all, or just sets a border
color to fake a mask.
This is why I put in the work tho, to prove we’re better than that. CSS allows us to do these things, so do them we shall!!
Footnotes
-
oh no it’s been very too long since that last post. I meant to get this out sooner but have been procrastinating with other projects in the meantime… figured that, while I was doing other work for my blog, I should at least write an article :P ↩