JavaScript Background Color Transition

“Wow! That’s really sweet!”

is what people say when they see the cool background color transition effect you’ll learn to make in this tutorial.

Let’s kick things off by taking a look at exactly what we’re coding today:

See the Pen JavaScript background color transition by Bojan Gvozderac (@IRIdev) on CodePen.0

It’s a pretty cool effect ( we use it on our Home Page ) that you can use as background for your website, on smaller sections of a website or even overlay a picture or some other content on a website, maybe lower the opacity and you got yourself an epic looking component! Enough chit-chat lets jump in to the code!

 

HTML and CSS

As you’ll see the HTML in this example is really ( REALLY ) basic. The only thing we need from our HTML is to setup div’s with 100 viewport height ( 100vh ) to have a nice amount of height to scroll through and to give them a data attribute containing ( in RGB format ) what color should the background be when that div is fully inside the viewport ( since the div’s are all 100vh ).

<div class="marker" data-color="254,254,254"></div>

<div class="marker" data-color="242,236,243"></div>

<div class="marker" data-color="133,227,188"></div>

<div class="marker" data-color="239,247,235"></div>

<div class="marker" data-color="222,189,180"></div>

<div class="marker" data-color="237,236,236"></div>

<div class="marker" data-color="242,236,243"></div>

<div class="marker" data-color="254,254,254"></div>

Our css is just this line of code:

.marker { height: 100vh; }

 

JavaScript

Now we’re getting to the fun part!
We’re gonna take things one step at a time, meaning, one code snippet at a time, FUN!

Let’s first initiate our variables and we’ll do that using this code:

var colorSections = Array.prototype.slice.call( document.querySelectorAll('[data-color]') );
var backgroundColorArray = [];
var backgroundMask = document.querySelector("body");

First we get all elements that have a “data-color” attribute ( our divs in this case ) since document.querySelectorAll() returns a NodeList not an array we need to slice the returned NodeList up and create an array using Array.prototype.slice.call and finally we store that array inside colorSections variable.
backgroundColorArray stores our BackgroundColorController class instances ( we’ll get to this in a second ) and backgroundMask is just the element that’s going to change it’s color, in this case it’s the body of our document but it can be other things.

I’ve been thinking, we should really wrap all of this up with something that’ll just be about changing the background color, but what? I KNOW! How about a class? Well… “class” since JS ( JavaScript, duh ) doesn’t support classes in the classical way ( it got a little bit better with ES6 and you can read up on JS classes here ). Anyway, we’ll be using the old way of creating JavaScript classes.
Let’s look at our BackgroundColorController class code!

var BackgroundColorController = function(elem, index, colorStart, colorEnd) {
 this.elem = elem;
 this.index = index;

 this.transitionRange = colorSections[index - 1].clientHeight;

 this.colorStart = colorStart;
 this.colorEnd = colorEnd;
 this.colorDif = {
 r: this.colorStart.r - this.colorEnd.r,
 g: this.colorStart.g - this.colorEnd.g,
 b: this.colorStart.b - this.colorEnd.b
 };

 this.colorTransition = {
 r: ( this.colorDif.r === 0 ) ? 0 : Math.floor( this.transitionRange / this.colorDif.r ),
 g: ( this.colorDif.g === 0 ) ? 0 : Math.floor( this.transitionRange / this.colorDif.g ),
 b: ( this.colorDif.b === 0 ) ? 0 : Math.floor( this.transitionRange / this.colorDif.b )
 }
}

Let’s think about this, what are we trying to do? We’re trying to get the backgroundMask ( in our case the document body ) element to transition its background-color from one color to another based on the scroll position in a nuanced way between divs, so how can we achieve this?
We’ll build a class with a constructor that takes in one of our divs with the color code, the divs index in the array ( backgroundColorArray ), the color from which it should transition and the color to which it should transition to.

Next, we need to do some math and calculate a few new variables, we’ll start with transitionRange.

transitionRange is the range over which the transition will occur, in our case it’s the height of a div which is set to 100vh, meaning if your viewport height is 800px then we will change the background-color from one color to another gradually over the span of those 800px

colorDif is the difference between colorStart and ColorEnd. We need this to know just how much each color will change when you scroll from one div down to another ( in other words, how much will each RGB color change during transitionRange )

colorTransition calculates at what points in the transitionRange do the colors change. This might not be clear to everyone so lets look at an example.

If the colorDif for the color red is 4 ( the color red changes by 4 between the starting color and the end color ) and the transitionRange is 800px then the color red should change by 1 every 200px in our transitionRange because 800 / 4 = 200
Notice that we first check if the colorDif of the colors are equal to 0 ( the color doesn’t change at all during transitionRange ), we need to do this since dividing by 0 isn’t defined in math and in our case it doesn’t calculate correctly and messes with our background color transition.
Also, we apply Math.floor() to the result so that it’s a nice round number.

 

Next on the list is the BackgroundColorController run function, this is where all the action happens. Let’s look at the code:

BackgroundColorController.prototype.run = function() {
  var boundingRectY = this.elem.getBoundingClientRect().top;

  if( boundingRectY < this.transitionRange && boundingRectY > 0 ) {

    var changeForR = ( this.colorTransition.r === 0 ) ? 0 : Math.floor( (this.transitionRange - boundingRectY) / this.colorTransition.r ),
        changeForG = ( this.colorTransition.g === 0 ) ? 0 : Math.floor( (this.transitionRange - boundingRectY) / this.colorTransition.g ),
        changeForB = ( this.colorTransition.b === 0 ) ? 0 : Math.floor( (this.transitionRange - boundingRectY) / this.colorTransition.b );

    var r = this.colorStart.r - changeForR,
        g = this.colorStart.g - changeForG,
        b = this.colorStart.b - changeForB;

    var colorChangeString = "rgb(" + r + ", " + g + ", " + b + ")";

    backgroundMask.style.backgroundColor = colorChangeString;
  }
};

We’ll add our run function to the BackgroundColorController prototype ( in JS this is the preferred way of expanding a class and adding new things to it, you can ready more about JS prototype object here )

First we’ll get the boundingClientRect for our element but only the top part since in our case we only need to worry about vertical positioning ( you can read more about boundingClientRect here ) and we’ll store it in boundingRectY variable.

boundingRectY is the top position of our divs relative to the viewport, because all our divs are 100vh this means boundingRectY can be any number between 0 and the height of our viewport ( notice this is also our transitionRange )

We check if a divs boundingRectY is between 0 and the height of our viewport if it is that means the backgroundMask should change its color based on that divs BackgroundColorController

The next thing we need to do is calculate at what stage of the transition we are in and how much the background color should change by that point ( changeForR/G/B ), we can do this by taking a divs transitionRange subtracting its boundingRectY, dividing that number with a colors colorTransition variable and then apply Math.floor() to the result, again, to make it a nice whole number.

var changeForR = ( this.colorTransition.r === 0 ) ? 0 : Math.floor( (this.transitionRange - boundingRectY) / this.colorTransition.r ),
    changeForG = ( this.colorTransition.g === 0 ) ? 0 : Math.floor( (this.transitionRange - boundingRectY) / this.colorTransition.g ),
    changeForB = ( this.colorTransition.b === 0 ) ? 0 : Math.floor( (this.transitionRange - boundingRectY) / this.colorTransition.b );

We now know how much the colors should change in that particular point. This probably isn’t going to be clear to everyone so lets look at some examples:

If our transitionRange is 800px, colorTransition.r = 200 and we just started a new transition ( what this means is that the bottom of our viewport is aligned with the top of a target div ) and the way boundingClientRect works at this point boundingRectY = 800, we now have this situation transitionRange – boundingRectY = 0 and 0 / 200 ( colorTransition.r, remember the formula ) = 0 so there is no change to the red part of the background color when we just started our transition, makes sense right?

What if we’re a little deeper in to our transition? Lets say we’re 200px in, now we have transitionRange = 800, boundingRectY = 600 ( 800 – 200 ) and colorTransition.r is still 200.
Our calculations now look like this ( 800 – 600 ) / 200 = 1, what this means is that at this point the red part of our background should change by 1 from the starting color ( colorStart ) and this leads us to our next bit of code

var r = this.colorStart.r - changeForR,
    g = this.colorStart.g - changeForG,
    b = this.colorStart.b - changeForB;

This is where we get our actual values for red, green and blue and the numbers we’ll use to color our background.
Because we’re transitioning from one color to another and we know the RGB of the starting color and we know how much a color should change at that scroll point, we subtract the starting color ( colorStart ) with the amount the each color should change at that point and we finally have the RGB for our backgroundMask for that specific scroll position ( YAY! ).

Only thing left to do is make a properly formatted background-color string and apply it to backgroundMask

var colorChangeString = "rgb(" + r + ", " + g + ", " + b + ")";
backgroundMask.style.backgroundColor = colorChangeString;

Lookin’ good!

 

We’re done with the code for BackgroundColorController class now we need to instantiate it for everyone of our divs with data-color, lets do that now:

colorSections.forEach(function(elem, index) {
 if( index > 0 ) {
 var colorStart = getSectionColorCode( colorSections[index - 1] );
 var colorEnd = getSectionColorCode(colorSections[index]);
 backgroundColorArray.push( new BackgroundColorController(elem, index, colorStart, colorEnd ) );
 }
}, this);

This part is pretty straight forward we just loop through our colorSections array and for each one we create a new BackgroundColorController class instance and we need to make sure we skip the first element in the colorSections array ( index > 0 ) since there isn’t a div before it for color transitioning.
That’s that! Wait… what the hell is getSectionColorCode() ?! Glad you asked, it’s a utility function that we use to extract the RGB color values from the divs data-color attribute and it looks like this:

function getSectionColorCode(sectionElem) {
 var colorString = sectionElem.getAttribute('data-color');
 var colorStringArray = colorString.split(',');
 var colorNums = [];

 colorStringArray.forEach(function(elem, index) {
 colorNums.push(parseInt(elem));
 }, this);

 return { r: colorNums[0], g: colorNums[1], b: colorNums[2] };
}

The function gets the data-color attribute of the passed element splits it using ‘,’ this creates an array with 3 values, we then loop through those values convert them to int and return a new object containing RGB color values. Simple, right?

We have just one more thing to do and that is to hook our classes up to the window scroll event so that it tracks when the users scrolls and fires of the background color change, we do it like this: 

window.onscroll = function() {

 backgroundColorArray.forEach(function(elem, index) {
 elem.run();
 }, this);

};

Aaaaaaand we’re done! Now you know how to make a sweet looking color transition using javascript! Good for you! Now you have just one more thing to do and that is to get creative and do some exciting things with this technique and be sure to tell me all about it in the comments below or email me or tweet @GvozderacBojan

Happy Coding!