When I launched this blog a couple years back, I wanted to add a bit of flair to the newsletter subscribe button. My idea: an animated rainbow gradient for the background.
I love gradients. After so many years of solid colors and flat design, I'm glad to see them making a comeback!
It turned out that animating CSS gradients was a lot more trouble than I expected, and the result was a little underwhelming:
Rather than animating the gradient directly, I created a very tall gradient, and translated it up within the button, resetting it once it neared the bottom. My trusty friend
overflow: hidden made sure that the excess wasn't visible to the user.
This approach kinda works, but there are problems:
- The looping isn't entirely seamless. Subtle differences in performance across devices means that it can be noticeable when the position resets.
- It just doesn't look that great; I wanted something with an organic kind of flowing quality, and this just felt static and lifeless.
Over the past couple years, I've given this button a lot of thought. It's been a long time coming, but after discovering a wild new technique, I was finally able to come up with something I like.
Without further ado, the new button:
Link to this headingRadial gradients to the rescue!
This new model uses a
radial-gradient: color seeps out from the top-left corner, shifting slowly through the rainbow, cascading across the button's surface.
More precisely, there's a 3-color radial gradient anchored in the top-left corner. The colors would all be adjacent in the rainbow, and each "tick" of the animation would shift the colors down:
The big difference here is that nothing is actually moving. There's no translate happening on a 2D plane anymore. Instead, I'm sampling 3 colors from a 10-color rainbow palette, and each point in the gradient is slowly shifting to inherit the color in the previous point. The
C3 point is always 1 color behind in the palette from the
This creates the illusion of motion, similar to those casino or venue lights:
This is also similar to how sound waves move through the air! I created an explorable explanation that demonstrates this concept.
Link to this headingAnimating gradients
So the game plan was coming together:
- I'd create a palette of 10 rainbow colors.
- I'd set a gradient to hold a moving window of 3 colors.
- I'd run an interval that would update the gradient every second, shifting each color by 1 spot.
- I'd tween between the colors in each spot. On every frame, the colors should inch towards their next value.
That last step was the trickiest. Unfortunately, you can't use
transition to interpolate between background gradients. The following snippet doesn't work:
I could do it all in JS. I could set up a
I wanted to do the interpolating in CSS. And happily, I found a way 😊
Link to this headingCustom properties (AKA CSS variables)
For a while now, CSS has had variables. At first blush, they look a lot like the variables you'd see in SASS or LESS, but unlike preprocessors, variables are still in the code at runtime. This makes them much more powerful, as we'll soon see!
Here's how you can use CSS custom properties in a gradient:
We can use inline styles to set this on React elements, like so:
On their own, this doesn't actually help us. We still can't apply
transition directly on
background. But it gets us one step closer 🕵🏻♂️
Link to this heading🎩 CSS Houdini
CSS Houdini is a wide-ranging set of upcoming CSS enhancements predicated on one idea: developers should be able to create their own CSS features.
For example, CSS doesn't have any built-in way to do masonry layouts. Wouldn't it be cool if you could build it, plugging in directly to CSS mechanisms, and then access it with
For another example: projects like Babel allow us to "polyfill" (most) missing features in JS, because we can mimic those new features using an earlier version of the language. But we can't polyfill (most) CSS features. Houdini will allow us to polyfill in missing CSS, by giving us access to the internal wiring of the CSS engine.
CSS Houdini is a huge project, already years into research and development, and I expect it'll shape the future of web development in exciting and unpredictable ways. For today, though, I'd like to focus on one relatively small but incredibly cool part of this: animated custom properties.
Link to this headingAnimated custom properties
In CSS, a "property" is something you can assign a value.
color are all examples of properties. Why, then, are variables in CSS called custom properties? Aren't they a totally different concept?
Actually, they're more similar than I realized. It's better to think of CSS variables as your own properties, like display and transform.
Here's the wild, mind-blowing part: you can transition custom properties:
We're not telling the browser to animate the background property, we're telling the browser to animate our custom property. And then we're using that custom property in our background gradient. Amazingly, the
var() keyword is reactive, causing the background to re-paint whenever the value changes, even when that value is being tweened by
My mind is still buzzing with the possibilities. CSS custom properties are so much cooler than I realized, and Houdini gives us downright magical powers ✨🧙✨
Link to this headingOne more piece: registering the property
There's one more thing we need to do before this will actually work. We need to tell the browser what the type of our custom property is.
Should the browser treat it as a color? A length? An angle? We need to be explicit about it, so that the browser knows how to interpolate changes.
We do this in JS with the following method:
Link to this headingA vanilla JS demo
In a bit, we'll see how React hooks let us package this up rather nicely. First, though, I wanted to share the raw JS code, for folks using a different framework, or no framework at all:
Link to this headingHook it up ⚛️
One of the neat things about React hooks is that they give the developer more control around how different ideas are expressed. Custom hooks let us shove a bunch of stuff in a box, and it's up to us to draw the boxes. We can choose whether we want to optimize for reusability, or clarity, or anything else.
In this case, I'd like to keep things friendly. I'm OK sacrificing a bit of power or flexibility in exchange for a no-fuss no-frills
Link to this headingState and API
Initially, I was thinking I would hold the current colors in state, but it occurred to me that the colors are derived data; the real bit of state is the current interval count.
If I'm on the 5th cycle, for example, I know that my colors will be the 5th, 6th, and 7th colors in my 10-color palette. Because the palette is static, I can just track that number, and use it to derive the colors.
The next thing I wanted to figure out was the hook's interface. I started by writing the component that will consume this hook. I like just making up whatever API seems ideal for the component that uses it. Consumer-driven development.
With that in mind, here's the initial version of this hook:
useIncrementingNumber is a custom hook that spits out a new, ever-increasing number, based on a provided interval delay. It's based off of Dan Abramov's setInterval hook. You can view its source here.
I like this approach, because there's a clear separation of duties:
useRainbowis in charge of generating and managing the colors, but has no vote in what they're used for.
- The component,
MagicRainbowButton, doesn't know anything about where these colors came from or when they update, but decides what to do with them.
There's one thing that makes my spidey-sense tingle a bit; it's pretty surprising that
useRainbow secretly registers global CSS custom properties. In fact, registering a global value from within an instanced component is going to be problematic! We'll tackle this, and some other lingering issues, in the next section.
Link to this headingMaking this production-ready
Before you start shipping rainbow buttons all over your law firm's website or your accounting software, there are a couple things we need to think about.
Link to this headingGlobal properties and duplicate components
The biggest problem with our current implementation is that it violates a core React principle: every instance of a component should be independent. We should be able to render as many copies of it as we want, without them interfering with each other.
If we try to render two copies of our
MagicRainbowButton on the same page, we get this error:
InvalidModificationError: Failed to execute 'registerProperty' on 'CSS': The name provided has already been registered.
This is because the CSS custom properties registry is a global object; all of our component instances are sharing the same global namespace! And right now, they're both trying to register the same names.
I got around this by creating a unique ID for each React component, and storing it with a
This also makes me feel better about the "secret side-effects in hooks" thing. A bit of randomness rules out the risk of name collisions, letting us pretend that it isn't actually global.
Link to this headingBrowser support
Houdini is super bleeding-edge, and this is reflected in its browser support: At the time of writing,
CSS.registerProperty is only supported by Chrome 78+, and Opera 65+.
My solution? Bail out of the hook early, if
CSS.registerProperty aren't found, and return the first 3 colors. Other browsers won't get the animation, but they'll still get a nice gradient! And our React component doesn't have to change at all 💯
Note: IE11 doesn't support custom properties at all, so if you need to support it, you'll need to set a fallback background gradient, using hardcoded color values instead of custom properties
Link to this headingPerformance
Last year, I gave a talk about animation/interaction performance. In that talk, I mention that there are two "gold standard" properties: opacity and transform. Those two properties perform way better than other properties, because they don't have to paint on every frame, they can be manipulated directly by the graphics card as a texture, shimmying around without the CPU doing any work.
In that talk, I also advocated for breaking this rule, as long as you're measuring. With a 6x throttle on my CPU, I fired up the profiler:
It is true that this technique involves a repaint on every frame, and that repaints can be slow… but in this case, the amount of repainting is tiny. The repaint takes ~0.3 milliseconds, which is about 2% of our budget if we want to hit 60fps.
Animating properties like
height is often very slow, because it involves both a layout and paint step, and because the number of pixels involved can be very large. In this case, there's no layout step, and the paint step is quick and targeted 💫
Link to this headingAccessibility
Whimsical touches are great, but not when they come at the expense of usability.
Certain types of animations can be problematic for folks with vestibular disorders—they can trigger vertigo, nausea, headaches, and other nasty symptoms.
Browsers have been hard at work implementing support for a "prefers-reduced-motion" media query. This query relies on a Windows/macOS setting, and lets users express that they would like to disable animations.
Here's how we can use it here:
First, we set a static gradient as the default. Then, if the user hasn't requested reduced motion, we apply our fancy dynamic version.
This method might seem backwards; wouldn't it make more sense to have the animated gradient be the default value, and then strip it out if
prefers-reduced-motion is set to
Well, let's consider what happens if the user is using an older browser, one which doesn't support the “prefers-reduced-motion” media query. For that user, everything inside the media query will be ignored. By structuring it so that the static version is the default, we ensure that users who can't express a preference receive the static version, not the animated one.
This is becoming less and less of a concern, since “prefers-reduced-motion” has been supported in all major browsers for several years, but I still prefer to err on the side of safety.
In addition to motion, we also need to think about color contrast. Will folks with vision impairments be able to read the text in the button? I added a bit of text shadow, and darkened the warm end of the spectrum. Truthfully, it may still be too low-contrast for certain periods in the animation, but I'm confident it's legible most of the time, and the animation shifts quickly.
Link to this headingConclusion
If you're keen to build your own rainbow button, the source code for this one might come in handy. This blog is open-source, so you can find it on Github.
It's a brand new year, and one of my goals for 2020 is to produce many high-quality interactive blog posts like this one. My newsletter is the best way to find out when something new is posted.
I know I've thrown a lot of "subscribe" buttons at you this post, but this last one is for real. Won't you join my newsletter?
February 22nd, 2024