I have a confession to make. You probably sensed it, but weren’t able to articulate what was happening. Your loved ones think you’re losing your grasp on reality. For the last six months I’ve been incrementally changing the color scheme on my website every single day. I boiled you like a frog! Mu-wa-ha-ha. Don’t believe me? Try for yourself…
At the center of this CSS-trick is setting a --hue
variable and rotating the hue about 1deg
each day to make it 360º around the color wheel over the year. Because I like blue, I set the default --hue
to 196
and add the current day’s --hue-rotate
value to calculate my final --rotated-hue
value.
--hue: 196;
--hue-rotate: <Floor ( CurrentDayOfYear * 365 / 360 )>; /* Math goes here */
--rotated-hue: calc(var(--hue) + var(--hue-rotate, 0)); /* Rotate it */
The math for the --hue-rotate
isn’t that complex, but since CSS doesn’t have a Date()
function, I set the initial --hue-rotate
value inside my Liquid template when I generate the site with Jekyll.
{% assign yearProgress = 'now' | date: '%j' %}
<style>
:root {
--hue-rotate: {{ yearProgress | times: 360 | divided_by: 365 }};
}
</style>
But I don’t post here every day, so I also update it with some JavaScript in the footer…
const dayOfYear = date => Math.floor(
(date - new Date(date.getFullYear(), 0, 0)) / (1000 * 60 * 60 * 24)
);
const yearProgress = dayOfYear(new Date()) / 365;
document.documentElement.style.setProperty(
'--hue-rotate',
Math.floor(yearProgress * 360)
);
Now my --rotated-hue
value nudges itself every day starting from the base hue.
I put --rotated-hue
inside an hsl()
and it worked okay, but was decidedly boring. I needed a way to make it more dynamic.
The next bit of magic was to use a slightly more complicated calc()
function for the saturation value. Using some of the new CSS math functions, I take the cos()
of the --rotated-hue
to get a value between 1
and -1
, then multiply that by 45%
which is my “base saturation” for my light mode theme. That gives me a value between -45%
and +45%
, so I double it and add another 45%
to avoid the gray zone of negative saturation.
:root {
color-scheme: light dark;
--hue: 196;
--rotated-hue: calc(var(--starting-hue) + var(--hue-rotate, 0));
--hue-rotate-deg: calc(var(--hue-rotate * 1deg)); /* Convert to degrees */
--hue-rotate-cos: cos(var(--hue-rotate-deg)) /* Magic */
--bg: hsl(
var(--rotated-hue)
calc(45% * var(--hue-rotate-cos) + 45%)
96.5%
);
}
That creates the following effect over time…
For my dark mode I wanted a darker website, so I lowered the base saturation to 25%
…
@media (prefers-color-scheme: dark) {
:root {
--bg: hsl(
var(--rotated-hue)
calc(25% * var(--hue-rotate-cos) + 12.5%)
18.5%
);
}
}
Astute readers will notice I don’t add the full base saturation back to the value. This was intentional because as I slid the --hue
value around I came across an ugly “background radiation” zone when --rotated-hue
enters the green color space (h=75-115). For a month or two my site looked like dogshit. I minimized the undesired effect by letting it dip down into the gray in dark mode.
As you can see in the chart above, lowering the base saturation creates a less dramatic wave function dipping below the 0%
saturation. The resulting effect is that my light and dark mode now share the same --rotated-hue
but have pretty different vibes. When I plot the two background saturations next to each other, you can see the relationship.
If someone asked me today “What color is your website?”, I sort of love the fact that I don’t know. In fact, I’d almost have to consult a chart on any given day…
Ah yeah. It makes perfect sense. It’s materialized color operating on the 49th vibration, you would make that conclusion walking down the street or going to the store.
Updating my accent color
The final step was improving my accent color. Finding a static color that had proper color contrast and worked well with every background color was difficult. I was hoping to find a shortcut but all roads led to making my accent color dynamic as well. How do I pick a color that works with every color? After some experiments, I leaned heavily on some third-grade art class color theory.
I found that rotating the --rotated-hue
another 240º to get a triadic tone on the color wheel gave the accent color the best chance at standing out in both light and dark mode. You can see the relation to the background and accent hue when I plot them next to each other.
I decided to use oklch()
instead of hsl()
for my accent color because I wanted the vibrancy that can only come from the p3
color space.
--accent: oklch(
75%
0.18
calc(var(--rotated-hue) + 240) / 1
);
I tinkered with picking the color by hand so that I could try and maximize the number of days my accent color punches up into the p3
space.
It’s more art than science here but I feel good about the results.
Rotating colors is fun but not without its challenges
As far as challenges go, accessibility and proper color contrast is a concern. Contrast checkers don’t work the best with p3
, but I’m sure that’s solvable. One challenge was my button text which I set in #FFFFFF
in attempt to maximize contrast. It’s not perfect but it’s right more than its wrong. This is one place where Safari’s CSS color-contrast()
would be welcome to have in all browsers.
I’ve also scoped my theme to two dynamic colors; adding more is difficult. I re-did my syntax highlighting theme and it’s doing okay (I used the other triadic tone), but more colors is more problems and the complexity balloons quick. Having some shades of gray in the style of Open Props would be helpful, so I may pursue that.
I’m happy with how it turned out but there’s plenty of room for improvement. I doubt I will ever stop obsessively tweaking the color formulas to get it juuuust right. One thing that bothers me now are the puke yellows in my accent color (like today’s) and the greens in the background color aren’t my favorite. I’m embracing the randomness, but I’d love a yellow like Piccalilli’s website. It might be possible to bump up the oklch()
brightness in certain parts of the color journey, but requires more math.
It would also be awesome to figure out how to change colors over a custom gradient as opposed to the hue wheel. Could I use a step-frame animation but grab a point on a conic gradient? Hm…
Was it worth it? Did anyone notice? Yes, one person noticed. My coworker John is the only person who said anything. Good eye, John.
All said and done, I like my little rotating color scheme. It’s fun waking up and wondering what color my website is today. If you’re having fun with color on your website I’d love to see it. I want all the p3
inspiration I can get.