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);
}
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%; }
}
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; }
}
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;
}
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();
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; }
}
For this to work smoothly, register the property:
@property --angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
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:
- Use
will-change: background(orwill-change: transformif you're moving the element) - Keep the animated element on its own compositing layer
- 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)