The short version
- Browsers cannot interpolate between two gradient values, so a CSS animated gradient moves, fades or transforms the gradient instead of animating it directly.
- The classic technique makes the background larger than the element and animates background-position, which works everywhere but repaints every frame.
- Registering custom properties with @property lets the browser animate individual colours, angles and positions inside a gradient.
- Animating transform or opacity on a separate gradient layer is usually the cheapest option, because browsers can often composite it without repainting.
- Every gradient animation should stop, or become nearly still, when the user has asked for reduced motion.
On this page
A CSS animated gradient is a gradient background that shifts colour or position over time. There is a catch: if you write two different gradients in a @keyframes rule, browsers do not blend between them, they jump. So every working technique animates something next to the gradient instead: its position, the custom properties inside it, or the layer it is painted on. Which one you choose decides how smooth it looks and how much work the browser does each frame.
For the design side, when motion helps a brand and how to pace it, see animated gradient backgrounds. This guide is the code.
Technique 1: move an oversized background
Make the gradient two to four times the size of the element with background-size, then animate background-position so a different part of it shows over time. It works in every browser and needs no JavaScript. Use alternate so the motion reverses smoothly instead of jumping back to the start.
.hero {
background-color: #1e1b4b;
background-image: linear-gradient(120deg, #1e1b4b, #7c3aed, #f472b6, #fb923c);
background-size: 300% 300%;
animation: pan 18s ease-in-out infinite alternate;
}
@keyframes pan {
from { background-position: 0% 50%; }
to { background-position: 100% 50%; }
}The cost: background-position is not something browsers can move on the graphics card alone, so the element is repainted on every frame. On a button that is nothing. On a full-screen hero it is real work that runs for as long as the page is open.
Technique 2: animate colour stops with @property
Custom properties are normally strings, which cannot be interpolated. Registering one with @property gives it a type, such as <color>, <angle> or <percentage>, and typed properties animate smoothly. Use them inside the gradient and you can animate the colours, the angle or the position of a stop directly. @property is supported in all current major browsers; check caniuse if you support older ones.
@property --from {
syntax: "<color>";
inherits: false;
initial-value: #7c3aed;
}
@property --to {
syntax: "<color>";
inherits: false;
initial-value: #f472b6;
}
@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 120deg;
}
.hero {
background: linear-gradient(var(--angle), var(--from), var(--to));
animation: shift 16s ease-in-out infinite alternate;
}
@keyframes shift {
to {
--from: #0891b2;
--to: #fb923c;
--angle: 200deg;
}
}This technique still repaints every frame, like the first. Its advantage is control: you can move one glow in a mesh gradient, rotate a conic border, or shift a brand gradient towards a second palette without any oversized backgrounds.
Technique 3: transform a gradient layer
Put the gradient on a pseudo-element much larger than the element, and animate its transform, for example a slow rotation or drift. Browsers can usually move a transformed layer on the graphics card without repainting it, so this is the cheapest way to animate a large gradient. will-change: transform hints that the layer should be promoted in advance.
.hero {
position: relative;
overflow: hidden;
isolation: isolate;
}
.hero::before {
content: "";
position: absolute;
inset: -50%;
z-index: -1;
background: conic-gradient(from 0deg, #1e1b4b, #7c3aed, #f472b6, #0891b2, #1e1b4b);
animation: turn 40s linear infinite;
will-change: transform;
}
@keyframes turn {
to { transform: rotate(1turn); }
}Technique 4: crossfade two gradients
Stack two gradient layers and animate the opacity of the top one. Opacity is also a compositor-friendly property, so the fade is smooth and cheap. It suits slow mood changes, such as a day palette fading into a night palette, and it lets the two gradients be completely different in structure.
.sky {
position: relative;
isolation: isolate;
background: linear-gradient(180deg, #7dd3fc, #fde68a);
}
.sky::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background: linear-gradient(180deg, #0f172a, #7c3aed);
animation: dusk 20s ease-in-out infinite alternate;
}
@keyframes dusk {
from { opacity: 0; }
to { opacity: 1; }
}Choosing a technique
| Technique | Browser work per frame | Best for |
|---|---|---|
Moving background-position | Repaint | Small elements, buttons, text |
@property colours and angles | Repaint | Precise colour changes, rotating borders |
transform on a layer | Usually composited only | Large, full-screen backgrounds |
opacity crossfade | Usually composited only | Slow mood or palette changes |
Measure rather than guess. In Chrome DevTools, the Rendering panel's paint flashing highlights areas repainted each frame, and a Performance recording shows how much of each frame goes to painting. CSS gradient performance walks through it.
Reduced motion and good manners
Some people get dizzy or distracted by background motion, and operating systems let them ask for less of it. Honour that with a prefers-reduced-motion query that stops the animation, leaving a still, finished frame. The reduced motion guide explains what counts as motion worth removing.
@media (prefers-reduced-motion: reduce) {
.hero,
.hero::before,
.sky::after {
animation: none;
}
}Avoid
- Cycles faster than about 8 seconds on large areas
- Rainbow hue rotation across the whole page
- Motion behind body text people need to read
- Animations that keep running when the tab is idle for minutes on end
Do
- Slow, ambient movement within one colour family
- Motion in the hero, stillness in the content
- A calm still frame for reduced motion
- Test on a modest laptop and phone, not just a fast desktop
Questions people ask
Can you animate a CSS gradient?
Not directly: browsers jump between two gradient values in keyframes. Animate background-position, registered custom properties used inside the gradient, or the transform and opacity of a gradient layer instead.
What is the smoothest way to animate a gradient background?
Animating transform or opacity on a separate gradient layer, because browsers can usually handle those without repainting.
How do I animate gradient colours in CSS?
Register each colour with @property using syntax: "<color>", use the properties in the gradient, and change them in @keyframes.
Do animated gradients slow down a page?
Small ones rarely matter. Large animated backgrounds that repaint every frame can cost battery and smoothness, so prefer transform or opacity and measure with DevTools.
How do I respect reduced motion for gradient animations?
Wrap an animation: none rule in @media (prefers-reduced-motion: reduce) so users who ask for less motion see a still frame.
Written by Gradiently
The team behind Gradiently, a design tool built around Marks: living gradients that make everything you design look like yours.
See our profile


