DEV Community

Snappy Tools
Snappy Tools

Posted on

CSS Gradient Animations: Three Techniques That Actually Work

Animated gradients are one of those effects that look impressive but have a surprising number of wrong ways to implement them. CSS can't natively interpolate gradient stop colours, which means you can't just write transition: background 0.5s and expect it to work. This post covers three techniques that do work, with the trade-offs for each.

Why transition: background Doesn't Work on Gradients

Browsers can animate background-color smoothly. But background: linear-gradient(...) is not a colour — it's an image value. CSS treats it the same way as background: url(photo.jpg). You can't smoothly transition between two image values, so the browser just jumps from one to the other with no animation.

/* This doesn't animate — it just snaps */
.box {
  background: linear-gradient(135deg, #6c63ff, #f43f8a);
  transition: background 0.5s; /* has no effect on the gradient */
}
.box:hover {
  background: linear-gradient(135deg, #f43f8a, #68d391);
}
Enter fullscreen mode Exit fullscreen mode

Here are three approaches that actually work.


Technique 1: Background-Position Shift (Pure CSS, Most Compatible)

The trick: make the gradient much larger than its container, then animate background-position. The gradient itself doesn't change — it just slides.

.animated-gradient {
  background: linear-gradient(
    270deg,
    #6c63ff, #f43f8a, #68d391, #6c63ff
  );
  background-size: 400% 400%;
  animation: gradientShift 6s ease infinite;
}

@keyframes gradientShift {
  0%   { background-position: 0% 50%; }
  50%  { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}
Enter fullscreen mode Exit fullscreen mode

How it works: The gradient is 4× the width of the element. The keyframes shift the visible window from left to right and back. Since the start and end colours match, it loops seamlessly.

Pro: Pure CSS, works everywhere, very smooth.

Con: The animation is a sliding motion, not a colour interpolation — different from a true colour-changing gradient. Also need to duplicate the start colour at the end for seamless looping.


Technique 2: Opacity Crossfade (True Colour Change, No JavaScript)

Layer two gradients using pseudo-elements and crossfade their opacity:

.crossfade-gradient {
  position: relative;
}

.crossfade-gradient::before,
.crossfade-gradient::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

.crossfade-gradient::before {
  background: linear-gradient(135deg, #6c63ff, #f43f8a);
}

.crossfade-gradient::after {
  background: linear-gradient(135deg, #f43f8a, #68d391);
  animation: crossfade 3s ease-in-out infinite alternate;
}

@keyframes crossfade {
  0%   { opacity: 0; }
  100% { opacity: 1; }
}
Enter fullscreen mode Exit fullscreen mode

How it works: One gradient sits on ::before, another on ::after. Fading the ::after opacity from 0 to 1 creates the impression that the gradient is changing colour.

Pro: Looks like a true gradient colour change. No JavaScript.

Con: Uses both pseudo-elements (can't use them for other purposes). Requires position: relative on the parent. You only get two states.


Technique 3: CSS Custom Properties + JavaScript (Most Flexible)

Define the gradient using CSS variables and update them in JavaScript:

.js-gradient {
  background: linear-gradient(
    var(--angle, 135deg),
    var(--color-start, #6c63ff),
    var(--color-end, #f43f8a)
  );
  transition: --angle 0.5s;
}
Enter fullscreen mode Exit fullscreen mode
const el = document.querySelector('.js-gradient');
let hue = 0;

function animate() {
  hue = (hue + 0.5) % 360;
  el.style.setProperty('--color-start', `hsl(${hue}, 70%, 60%)`);
  el.style.setProperty('--color-end', `hsl(${(hue + 120) % 360}, 70%, 60%)`);
  requestAnimationFrame(animate);
}

animate();
Enter fullscreen mode Exit fullscreen mode

How it works: CSS custom properties can be transitioned if registered with @property (Houdini). Even without registration, JavaScript updates them in a requestAnimationFrame loop creating smooth frame-by-frame changes.

Pro: Complete control. You can animate angle, colour, number of stops, or anything. Works with any gradient shape.

Con: Requires JavaScript. For the transition on custom properties to work, you need @property (supported in Chrome and Edge, not Firefox as of 2024).


Animating the Gradient Angle

All three techniques can animate the gradient direction. The simplest approach with pure CSS:

.rotating-gradient {
  background: conic-gradient(
    from var(--angle, 0deg),
    #6c63ff, #f43f8a, #68d391, #6c63ff
  );
  animation: rotate 4s linear infinite;
}

@keyframes rotate {
  to { --angle: 360deg; }
}
Enter fullscreen mode Exit fullscreen mode

For this to work smoothly, register the property:

@property --angle {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}
Enter fullscreen mode Exit fullscreen mode

Without @property, the conic rotation will jump rather than animate smoothly.


Performance Note

Gradient animations trigger a repaint on every frame. To keep performance high:

  1. Use will-change: background (or will-change: transform if you're moving the element)
  2. Keep the animated element on its own compositing layer
  3. Prefer opacity/transform changes over background changes where possible — those run on the GPU compositor thread

For UI backgrounds and hero sections, all three techniques are fast enough on modern hardware. For 60+ simultaneous animated gradient elements, consider WebGL.


Quick Reference

Technique JavaScript? Looks like Compatibility
background-size shift No Sliding gradient All browsers
Opacity crossfade No Colour change All browsers
CSS variables + JS Yes Anything Modern browsers
@property + conic No Rotating gradient Chrome/Edge only

For a visual gradient editor that generates the base CSS for any of these techniques, use the free CSS Gradient Generator at SnappyTools — copy the CSS and wrap it in the animation keyframes shown above.

Top comments (0)