Create a Mixing Gradient Animation with CSS

I’ve gone through several logos for this personal blog of mine, but I’ve never been quite satisfied with any of them. I finally honed-in on the “dotted-T”, but then came the really tough decision.

What color will it be?

Well, why choose one? I can make it transparent and dynamically change the background element behind it depending on the season. But then I got another idea, and after playing around with some CSS animations for a few hours, I came up with a way to make this blending colors effect.

Or “mixing gradients” effect. Honestly I don’t know what to call it. I asked a lot of people to identify the effect and got everything from “Morphing Gradient Animation” to “a cross between aurora borealis and the Wicked Witch’s crystal ball in Wizard of Oz.”

Regardless, I think the effect is neat and I want to show how I did it.

Warning: This page repeats a lot of CSS animations that may cause your browser to consume a large amount of resources. If you want, skip down to the final result.


Playing With Blocks

Create a container DIV. This will control the size of all nested objects so that you can scale the entire effect by only adjusting the size of the container.

Inside the container we’ll have three other DIVs for our three colors (magenta, indigo, and lime) that are mixing around.

<div class="container">
	<div class="color c1"></div>
	<div class="color c2"></div>
	<div class="color c3"></div>
</div>

There are four classes: a container class, a color class, and then three color-subclasses c1, c2, and c3. That’s all you need in your body for right now. The rest is CSS.

So, let’s start off simple:

.container {
	background-color: rgb(255,255,255);
	margin: 0 auto;
	position: relative;
	height: 200px;
	width: 200px;
}

.color {
	height: 100%;
	position: absolute;
	width: 100%;
}

.c1 {
	background: rgba(0,255,159,0.6);
}

.c2 {
	background: rgba(0,159,255,0.8);
}

.c3 {
	background: rgba(255,31,159,0.8);
}

It’s a bit important to understand what’s happening here if you want to customize this for your own use. All three nested DIVs in the container are of class color, which has a position property “absolute”, therefore each of those DIVs origin position (0,0) is the same as the next “relative” positioned element, which is the container. The result is that all three elements overlap themselves and the container.

Then for each subclass c1, c2, and c3, we set a semi-transparent color of lime, indigo, and magenta respectively. ¡Voila!


Well, no, it’s not that impressive. It’s a pink-ish-purple square.

Change c2 and c3‘s background from just a color to a radial-gradient that’s fixed to different corners.

.c2 {
	background: radial-gradient(circle at top right, rgba(0,159,255,0.8) 30%, rgba(0,159,255,0) 70%);
}

.c3 {
	background: radial-gradient(circle at top left, rgba(255,31,159,0.8) 30%, rgba(255,31,159,0) 70%);
}

This still doesn’t look like much, but that’s okay! Note that we didn’t apply a gradient to c1. The reason will become clear once we animate this.


Mixing Things Up

Now the fun part! The biggest part of the illusion of mixing is getting these translucent blocks to rotate, particularly in an apparent-random sequence. For that, we need to establish some @keyframes.

@keyframes spin { 
	0% { transform: rotate(0deg); }
	20% { transform: rotate(-90deg); }
	40% { transform: rotate(60deg); }
	60% { transform: rotate(-180deg); }
	80% { transform: rotate(-270deg); }
	100% { transform: rotate(0deg); }
}

What I’ve done is created an animation sequence, named “spin”, that applies a CSS transformation in various increments. The radial degrees I used will give the appearance of a random amount of rotation, even though it’s the exact opposite of random.

Here’s what happens when you apply these keyframes to the color class. Remember, all three inner DIVs are of color class.

.color {
	animation: spin 30s ease-in-out infinite;
}

That’s… something! The problem here is that all the inner DIVs are rotating at the same time. Rather than apply the animation attributes to the color class, only apply the animation to the subclasses that have gradients, c2 and c3 (indigo and magenta). For the time intervals, prime numbers work well to minimize the instances the keyframes overlap.

.c2 {
	animation: spin 29s ease-in-out infinite;
}

.c3 {
	animation: spin 41s ease-in-out infinite;
}

Maybe now you see why there’s no gradient on the lime square (and why it’s not rotating). Occasionally, all three corners with gradients would line up and all color would shift to one side.

Now, apply a border-radius to all objects to make circles.


This works well enough, but sadly it’s still fairly obvious that there are color elements rotating around.


Last Steps

If you stare at it long enough, you’ll start to see that the lime disappears a lot, indigo a little, but magenta never. That’s simply the reality of overlapping DIVs rotating in place. We can aid in the illusion by gradually and periodically fading the top-most element.

Let’s add another animations to briefly reduce the opacity of the c3 (magenta) element. This should help with the “mixing” illusion.

@keyframes fade { 
	0% { opacity: 1; }
	20% { opacity: 1; }
	35% { opacity: 0.5; } /* moment of least opacity */
	50% { opacity: 1; }
	100% { opacity: 1; }
}

You can have multiple animations apply to a single element as long as you don’t declare the animation property twice. Just separate each definition with a comma.

.c3 {
	animation: spin 41s ease-in-out infinite, fade 17s ease-in-out infinite;
}

That’s better!

Now for the illusion to work best, the color sphere needs to be obscured by something that will divide the watcher’s attention. I used a transparent PNG with my logo in this site’s header (did you notice??), but you can use other CSS tricks.

My personal favorite is a callback to the original Mac OS X “Aqua” interface.

Add three more numerated color subclasses c4, c5, and c6.

<div class="container">
	<div class="color c1"></div>
	<div class="color c2"></div>
	<div class="color c3"></div>
	<div class="color c4"></div>
	<div class="color c5"></div>
	<div class="color c6"></div>
</div>

c4 will be the Aqua bubble’s reflection from a fictional overhead light source, c5 a diffused reflection passing through the bottom of the bubble, and c6 will be an inset shadow to really drive home the illusion of three dimensions.

.c4 {
	background: linear-gradient(rgba(255,255,255,0.7) 20%, rgba(255,255,255,0) 70%);
	width: 60%;
	height: 50%;
	margin: 5% 20%;
	border-radius: 50%;
}

.c5 {
	background: radial-gradient(circle at bottom, rgba(255,255,255,0.8) 10%, rgba(255,255,255,0) 40%);
}

.c6 {
	box-shadow: inset 0 0 20px rgba(0,0,0,0.8);
 }

… and now you have a 100% CSS-generated, cross-browser compatible, mixed-gradient, aqua-throwback, animated bubble slash crystal ball… thing.


To paraphrase Steve Jobs, it’s a DIV that looks so good you’ll want to lick it.

The Code

HTML:

<div class="container">
	<div class="color c1"></div>
	<div class="color c2"></div>
	<div class="color c3"></div>
	<div class="color c4"></div>
	<div class="color c5"></div>
	<div class="color c6"></div>
</div>

CSS:

.container {
	background-color: rgb(255,255,255);
	border-radius: 100%;
	box-shadow: 0 5px 10px rgba(0,0,0,0.3);
	margin: 0 auto;
	height: 200px;
	position: relative;
	width: 200px;
}

.color {
	border-radius: 100%;
	position: absolute;
	height: 100%;
	width: 100%;
}

.c1 {
	background: rgba(0,255,159,0.6);
}

.c2 {
	animation: spin 29s ease-in-out infinite;
	background: radial-gradient(circle at top right, rgba(0,159,255,0.8) 30%, rgba(0,159,255,0) 70%);
}

.c3 {
	animation: spin 41s ease-in-out infinite, fade 17s ease-in-out infinite;
	background: radial-gradient(circle at top left, rgba(255,31,159,0.8) 30%, rgba(255,31,159,0) 70%);
}

.c4 {
	height: 50%;
	width: 60%;
	background: linear-gradient(rgba(255,255,255,0.7) 20%, rgba(255,255,255,0) 70%);
	margin: 5% 20%;
	border-radius: 50%;
}

.c5 {
	background: radial-gradient(circle at bottom, rgba(255,255,255,0.8) 10%, rgba(255,255,255,0) 40%);
}

.c6 {
	box-shadow: inset 0 0 20px rgba(0,0,0,0.8);
}

@keyframes spin { 
	0% { transform: rotate(0deg); }
	20% { transform: rotate(-90deg); }
	40% { transform: rotate(60deg); }
	60% { transform: rotate(-180deg); }
	80% { transform: rotate(-270deg); }
	100% { transform: rotate(0deg); }
}

@keyframes fade { 
	0% { opacity: 1; }
	20% { opacity: 1; }
	35% { opacity: 0.5; } /* moment of least opacity */
	50% { opacity: 1; }
	100% { opacity: 1; }
}