An animated radial gradient mask over text in CSS
I saw a cool text effect on Nikhil’s blog recently, where the big “Hello!” on his website has some rings masking the word “hello” that produce a cool effect, and I thought I’d try to replicate it.
Here’s the final result, which I’ll explain further, and I want you to open it and be ready to poke at it and change some things:
See the Pen Radial gradient mask over text by Cassidy (@cassidoo) on CodePen.
It was a fun little experiment!
The font
The main thing that was particularly (and surprisingly) challenging was picking a font that worked well. Whatever font I picked needed to have a perfectly circular letter “o” for the “stripes” to show up well. I used Fredoka because it had that circular “o” character, and I liked that it was a nice rounded sans serif font.
CSS mask + repeating-radial-gradient
The CSS mask property lets you mask/clip elements. It’s typically used on images, but doesn’t have to be!
Now when I say “mask” or “clip” here, I mean literally hiding aspects of it. The effect you see above is called “alpha masking”, where transparent areas hide parts you don’t want to see, and opaque areas show said parts.
The line you want to look for in the CSS is line 25:
mask: repeating-radial-gradient(
circle at 198.2px 69.6px,
black,
var(--stripes),
black,
0,
transparent,
calc(var(--stripes) * 2),
transparent 0
);
This is a bit intimidating, so before I go deeper into explaining it, replace that line with this:
mask: linear-gradient(transparent, black);
You’ll see the text be a simple top-to-bottom gradient, where the top is transparent, and the bottom is that off-white color.
You can even replace the color black with red or cyan or rebeccapurple, and it’ll look the same, because as long as it’s not transparent, the mask will work.
Now, if you swap back to our previous example:
mask: repeating-radial-gradient(
circle at 198.2px 69.6px,
black,
var(--stripes),
black,
0,
transparent,
calc(var(--stripes) * 2),
transparent 0
);
Let’s break it down:
There’s a circle at a very specific point as the origin, and that was literally me just slowly and manually adjusting the coordinates until the gradient was centered around the letter “o”. It was tedious. Every time I changed my font choice or size, I had to redo this part. If you change the <h1> in the HTML to say anything else, you will have to do this again. Wee!
The variable --stripes is initially 2px, so every two pixels, there is a rendered repeat of either a white stripe, or a transparent stripe.
If you wanted to make this more intimidating and less legible and harder to maintain, you could use the shorthand for transparency #0000 and the hex code for black #000, and it produces the same effect:
mask: repeating-radial-gradient(
circle at 198.2px 69.6px,
#000,
var(--stripes),
#000,
0,
#0000,
calc(var(--stripes) * 2),
#0000 0
);
Why would you do that, you may ask? Answer: For evil.
More on @property --stripes
You can experiment more with how the rings on top of the word show up by messing with --stripes. At the top of the CSS, you can change it to be 3px or even 10px just to see how the circles get thicker.
But also, if you’ll notice that calc(var(--stripes) * 2) part of the gradient, that’s fun to poke at, too. If you change that 2 to 4, it spreads the lines out further apart. If you go much higher than that, the words are illegible, but kind of interesting still.
Something that I really want to point out here is that I defined --stripes here as a @property instead of a regular ol’ variable. This is because I wanted to animate the effect! As I was experimenting with this, I initially did have --stripes as a CSS variable.
But… animating is funky. Typically it’s not easy to animate CSS gradients. CSS treats gradients as images, rather than paths of color, so transitioning between states is hard for a browser to do. The (new-ish, widely available as of 2024) CSS Properties and Values API (as a part of CSS Houdini) lets us register custom properties as animatable colors. So, in the demo, if you uncomment out the animation on line 24, you can see the gradient animate in and out!
Browser compatibility
Bleh, Firefox and Safari did not like this demo at various points. I had to do a lot of trial and error because they render things differently. Luckily they support most of the final result, but I ran into more issues than I expected.
Using units like rem in the gradient (for some reason) made Firefox mad, and the animation on Safari working is an actual miracle after messing with sizing and pixel pushing for… far too long. I still think I could change the gradient position a bit more just for those browsers, but I am gonna call it “good enough” right now.
I’m certain there’s a better way to write this so that I can reuse code better (I probably could have more HTML than just a single <h1>, I could probably use a mask-position or something to help, etc), but for this tiny window of time, this works!
Wee
I hope you enjoyed poking around on this project with me! I love this stuff and learned a lot about @property, masks, and repeating gradients in the process.
Byyyyye, keep climbing your mountains!