useRainbow()

I took a break from work and started some small, personal projects (toys). One of those small projects is potato.horse where I keep all of my doodles, visual short stories and jokes. Check it out!

However, this post is not about my break from work, other experiments, or the site itself. People seem to like one particular technique I used in the design, notably, the background effect applied that transitions between colors when the user browses the content:

Some asked me how this effect was implemented (including going as far as reading the minified code, which is very flattering).

So, here’s a quick gist, followed up with some context:

export const useRainbowBg = () =>
useEffect(() => { const cb = () => { const viewportHeight = window.innerHeight const contentHeight = document.body.getBoundingClientRect().height const viewportsPerRotation = Math.min( 3, contentHeight / viewportHeight ) const from = 51 const progress = window.scrollY / (viewportHeight * viewportsPerRotation) const h = (from + 360 * progress) % 360 document.body.style.backgroundColor = `hsl(${h}deg, 100%, 50%)` } window.addEventListener('scroll', cb, { passive: true }) return () => window.removeEventListener('scroll', cb)
})

In short, I map the scroll position into the hue in the HSL color notation. Let’s break this down.

Color models

There are many ways of describing colors in CSS, with the two most common ones being RGB (left) and HSL (right):

RGB is an additive color palette. This means that mixing 100% of red, green and blue produces white, mixing 100% red and 100% green but 0% blue produces yellow, and so on. This is different from, say, using oil paint or the CMYK color model, where the resulting tone would be black(-ish)1.

We’re used to this approach because it’s easy to describe in code, but specifying colors in terms of hue, saturation and luminosity seems more natural, especially if you come from a design background, or… you know, are a human being using a human language.

We’ve gotten used to RGB as developers, but in spoken language, using it would feel unnatural and confusing. Façade would be very hard to use in RGB.

On the other hand, HSL can often be much more intuitive to work with. For instance, if I want to make a color slightly colder, I can just move the hue slider a bit towards blue and I should get closer to what I have in mind. With RGB, if we make the color appear colder by including more blue, the resulting tone will be a bit brighter as the blue component contributes to the overall lightness. This means that you’d have to lower the red and green values to compensate.

To see how this works in practice, try maxing out the blue color in the example below.

The first thing that stands out is that all tones are shifted towards blue and the overall brightness of the picture is increased. In the case of the effect we’re discussing, that would be undesirable.

Now, let’s try to do the same with the HSL color circle. Drag the slider to the left, by ca. 90 degrees:

In this scenario, using HSL not only turns Susan into a vampire, but also maintains a similar2 level of brightness. And that’s exactly what I’m looking for.

So, what I mean by saying this:

How does this work? In short, I map the scroll position into the hue in the HSL color notation. Rafal, 2 days earlier

…is that that every time we detect a scroll event, I try to map it to an angle on the hue circle:

I didn’t want to start with red as it would make me hungry and the base yellow fits the design a bit better, so I applied a small initial shift—hence const from = 51 set as the initial offset.

And, as far as the basic implementation goes, that’s it!

Now, there are three other areas of improvement.

useRainbow performance

We’re triggering a repaint on every scroll, so I was a bit worried that older mobile devices, or even some hi-end laptops plugged in to 4k screens might not be able to maintain solid 60fps. But, I’m happy with the results so far. Using passive event listeners provided a bit of a boost, especially on mobile.

If I realize that performance is a problem, especially with more content down the line, I’ll probably focus on:

  • removing the unnecessary call to getBoundingClientRect on every scroll handler call, and
  • deferring or throttling background color changes using requestAnimationFrame.

I expect the first improvement to have some impact, but the benefits of the second one should be negligible.

Measure before optimizing. Obsessing about the performance only makes sense when issues become noticeable, be it through a drop in framerate or battery impact. Your iPhone Pro has more computing power than many low-end laptops, so it’s a good idea to test on those devices too. It’s good to have a crappy old Android phone exacly for that purpose if you can spare a few quid.

Perceptually uniform color spaces

You might’ve noticed that in the previous illustrations some fully saturated colors seemed darker than others. That’s because the color spaces we normally use when coding don’t reflect the way the human eye works. I’ll leave the in-depth explanation to someone much more experienced than me, but suffice to say (gross oversimplification alert!) that, generally, the same amount of red/green/yellow will appear brighter than blue. This means that in some cases the text on the page will be harder to read.

For now, this isn’t an issue as I’ve just put this thing online and titles serve a secondary purpose. But there’s a solution to the problem and it’s not overly complicated: use a perceptually uniform color space. There’s a bunch of libraries that do it out of the box, both in JavaScript/TypeScript and CSS/Sass/<pick your CSS flavor here>. hsluv seems like a good starting point.

Accessiblity

Note that I’ll be focusing on the visual effect itself and not discussing the rest of the site (e.g. alt tags, document structure, etc…). I’d like to focus on contrast, color blindness and people who rely on prefers-reduced-motion. The site is a living document; there’s always so much to improve. For instance, contrast can be an issue in a few, non-critical places. I’m happy to accept feedback and implement it: hit me up!.

color blindness

I wanted to make sure that the effect doesn’t break the site completely for people with color blindness. So I focused on the most common types: deuteranomaly and protanomaly (red-green color blindness), but also ran wider tests. I used Photoshop and Colorblindly (Chrome extension) for some rudimentary checks.

prefers-reduced-motion

The prefers-reduced-motion CSS media feature is used to detect if the user has requested that the system minimize the amount of non-essential motion it uses.

MDN

This site doesn’t contain many animations (besides the Little Sausage Angels you’ll see if you hit “Share”), but I was wondering if people who rely on prefers-reduced-motion would like the background color to stay constant.

The short answer is: I don’t know. My intuition is that rotating colors don’t really qualify as motion, but my experience and understanding of the problem is, to say the least, limited. In situations like this, I’d rather depend on user research than guesses.

Luckily, the site had its five minutes of fame on Reddit which proved to be a decent opportunity to collect feedback. None of the users brought up an issue with the background effect so far. I’m also lucky enough to know a bunch of accessibility specialists, such as Sandrina Pereira. Her suggestion was that (a) background animations definitely qualify as motion, and (b) perhaps the effect feels natural because it’s a direct result of a user interaction.

Summary

The late-90s Geocities web felt playful and weird. It was fun in an uninhibited, somewhat less performative, way. I wanted to incorporate some of this look and feel in the site. But still, I didn’t want to make it feel esoteric to the point where you’d need to up your hipsterdom-level to 9000 and browse it exclusively throught Netscape 7. All of that, while listening to the new Nirvana Unplugged album.

I still wanted decent UX on mobile and desktop, and some space for easter eggs (something you can’t do when living in the strange and abusive relationship with social media we’ve grown so accustomed to).

As a kid, I had built six websites before I even got access to the Internet for the first time. Now, after being burned out for three years, even considering changing my job, it was the first time I genuinely enjoyed coding. I forgot how much fun it was!

Now, go out, pet your cat, and make stuff!


P.S. Check out Cameron’s World.

P.P.S. The code for interactive diagrams can be found on GitHub.

Footnotes

  1. Hence the K component in CMYK meaning “black.” Using B would be confusing as it means “blue” in other color models.
  2. It’s not perfect since the perceptual color space differs from what’s described using RGB/HSL.

useRainbow() originally published on CSS-Tricks. You should get the newsletter and become a supporter.