Smooth CSS Gradient Transitions

Johnny Simpson - Feb 12 '22 - - Dev Community

If you try to animate or transition a gradient with just CSS, you end up with an issue - all modern browsers do not natively transition colours in gradients smoothly. As such, if we try to hover or interact with an element which transitions from one gradient to another, we end up with a sudden change, even when used with something like transition: all 0.1s ease-out.

Animating and Transitioning Gradients with CSS and Javascript

In this article, we'll be looking at how to solve this problem, and we'll cover how to smoothly animate a gradient transition with Javascript. That means creating a function which will be able to transition between two colors smoothly.

The full code for all of this can be found on codepen here.

Although there is no native way to do this effect, we can do it with Javascript. The below button solves the problem, allowing us to smoothly animate gradient transitions with Javascript and some CSS when you hover over the button.

Transitioning between two gradients in CSS smoothly #
The first step, is we need to create a function which allows us figure out a color between two colors. For this to work, we'll need the color we start with, and the one we want to transition to.

The function we'll create is shown below. It can support gradients with more than 2 colors - but for this we'll just use two. We'll also take the initial gradient color, and apply that to our button - so we can manipulate the gradient completely from our Javascript.

let element = 'gradient-button-transition'; // <-- id of the button we're transitioning

// DEFINE YOUR GRADIENT COLORS HERE
// Pct refers to the percentage position of the gradient stop point.
const gradientStopOne = [
    { pct: 0,  color: { r: 255, g: 113, b: 71 } }, // The first color in your gradient
    { pct: 100, color: { r: 0, g: 55, b: 255 } }   // The color you want your first color to transition to
];
const gradientStopTwo = [
    { pct: 0,  color: { r: 224, g: 65, b: 127 } }, // The second color in your gradient
    { pct: 100, color: { r: 0, g: 173, b: 255 } }  // The color you want your second color to transition to
]

// Apply our gradient programmatically so we can completely manipulate the gradient from JS rather than CSS
let c1 = gradientStopOne[0].color;
let c2 = gradientStopTwo[0].color;
document.getElementById('gradient-button-transition').style.background = `linear-gradient(${angle}deg, rgb(${c1.r}, ${c1.g}, ${c1.b}), rgb(${c2.r}, ${c2.g}, ${c2.b}))`;

// This function transitions between two rgb colors
const getColor = function(pct, colorSet) {
    for (var i = 1; i < colorSet.length - 1; i++) {
        if (pct < colorSet[i].pct) {
            break;
        }
    }
    // This conversion figures out the transition between two rgb values
    var lower = colorSet[i - 1];
    var upper = colorSet[i];
    var range = upper.pct - lower.pct;
    var rangePct = (pct - lower.pct) / range;
    var pctLower = 1 - rangePct;
    var pctUpper = rangePct;
    var color = {
        r: Math.floor(lower.color.r * pctLower + upper.color.r * pctUpper),
        g: Math.floor(lower.color.g * pctLower + upper.color.g * pctUpper),
        b: Math.floor(lower.color.b * pctLower + upper.color.b * pctUpper)
    };
    // And returns the rgb code
    return `rgb(${color.r}, ${color.g}, ${color.b})`;
}
Enter fullscreen mode Exit fullscreen mode

Now that we have a function which will let us transition between two colors, and have defined our gradients, We can start transitioning between them. We'll create one function which will set an interval - depending on whether the user hovers or not, we will manipulate the direction of the animation.

Comments in the code below explains what we're trying to do here. The interval runs every 16.67 miliseconds - or 60 times a second. This will give us a smooth 60 frames per second animation. Within the interval function, we calculate the total number of frames, and stop the animation when the transition time is up.

let transitionTime = 1000           // <-- 100 ms - time our animation will last
let previousTime, start = 0;        // <-- stores data on animation
let angle = 180;                    // <-- angle of gradient
let animationDirection = 'forwards' // <-- stores the animation direction
let intervalFrame;                  // <-- stores the interval frame
let currentPct = 0;                 // <-- current percentage through the animation
let elapsed = 0;                    // <-- number of frames which have ellapsed 

// This is our animation which we run on hover
const animateGradient = function() {
    if(intervalFrame === undefined) {
        intervalFrame = setInterval(() => {
            let time = transitionTime / 1000; // time in seconds
            let numberOfFrames = time * 60; // 60 frames per second -> 1 second = 60 frames

            // If the animation is going forward
            if(animationDirection === 'forwards') {
                // Add 1 to elapsed
                elapsed += 1;
                // The elapsed frames out of max frames
                currentPct = Math.min(elapsed / numberOfFrames, 1) * 100;
            }
            else {
                // Otherwise we're going back - subtract 1 from ellapsed
                elapsed -= 1;
                // The elapsed frames out of max frames
                currentPct = Math.max(elapsed / numberOfFrames, 0) * 100;
            }

            // Calculate current color in this time for each gradient color
            let colorOne = getColor(currentPct, gradientStopOne);
            let colorTwo = getColor(currentPct, gradientStopTwo);

            // Generate CSS string
            let generateGradient = `linear-gradient(${angle}deg, ${colorOne}, ${colorTwo})`;

            // Add it to our background.
            document.getElementById(element).style.backgroundImage = generateGradient;

            // End the interval when we're done
            if(currentPct === 100 || currentPct === 0) {
                clearInterval(intervalFrame);
                intervalFrame = undefined;
            }
        }, 16.667); // 60 frames per second
    }
};
Enter fullscreen mode Exit fullscreen mode

Finally, we run all of this on hover in, and hover out. When the user hovers, we update the animation direction, so we can move the gradient towards the colors we want it to.

// On hover, run our animation
document.getElementById('gradient-button-transition').addEventListener('mouseover', function() {
    animationDirection = 'forwards';
    animateGradient();
});
// On hover out, run our animation again, but backwards
document.getElementById('gradient-button-transition').addEventListener('mouseleave', function() {
    animationDirection = 'backwards';
    animateGradient();
});
Enter fullscreen mode Exit fullscreen mode

Multiple color gradient transitions

Since we can have this run for multiple colors, and also run whenever we want - we can create some fun effects. Here is a button that automatically transitions between 4 different gradients:

Conclusion

Although not possible with CSS today, Javascript actually gives us a lot more flexibility to animate our gradient transitions smoothly. If you've found this useful, don't forget to subscribe or follow me on twitter.

You can find the full code on codepen here.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .