In this guide
What you will learn
- Why You Cannot Animate a Gradient Directly
- The Classic Animated Gradient CSS Technique
- Timing and Subtlety
- Respect prefers-reduced-motion
An animated gradient can make a hero section feel alive, or it can make a page feel distracting and slow. The difference comes down to technique and restraint. This guide explains how the classic animated gradient CSS pattern works, how to time it so it stays subtle, how to respect people who prefer reduced motion, and when to switch to compositor-friendly layers or @property.
Why You Cannot Animate a Gradient Directly
CSS gradients are images, not colors. Browsers interpolate background-color smoothly, but the specification treats background-image as discretely animated, so you cannot rely on a smooth blend from one linear-gradient() definition to another. Every CSS gradient animation therefore uses one of three workarounds:
- Make the gradient larger than the element and pan across it with
background-position. - Put the gradient on its own layer and move or fade that layer with
transformoropacity. - Register custom properties with
@propertyso the browser can interpolate an angle, color or stop position inside the gradient.
The Classic Animated Gradient CSS Technique
.hero {
background: linear-gradient(120deg, #6366f1, #a855f7, #ec4899);
background-size: 400% 400%;
animation: gradient-shift 18s ease-in-out infinite;
}
@keyframes gradient-shift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
Two properties do the work together:
background-size: 400% 400%makes the gradient image four times the size of the element in each direction, so only part of it is visible at any moment.background-positionpans that oversized image. A percentage position aligns the same point of the image and the element, so0% 50%shows the left side of the gradient and100% 50%shows the right side.
Without the larger background-size, nothing moves. When the image is exactly the size of the element, every percentage position resolves to the same place, so there is nothing to pan across. Because the background shorthand resets background-size, always declare the size after the shorthand.
The animated gradient generator builds exactly this pattern: choose three colors, an angle and a duration in seconds, watch the animated preview, and copy complete CSS with background-size: 400% 400%, the @keyframes block, the animation line and a prefers-reduced-motion rule.
Make the loop seamless
The keyframes above travel out and back, so the loop never jumps. A shorter equivalent is alternate with only two keyframes:
.hero {
animation: gradient-shift 9s ease-in-out infinite alternate;
}
@keyframes gradient-shift {
from { background-position: 0% 50%; }
to { background-position: 100% 50%; }
}
Each 9-second pass runs in one direction and the next pass reverses it, so a full round trip still takes 18 seconds. If you animate in one direction only and let the cycle restart, the background snaps back to the start each time, which is exactly the jolt you are trying to avoid.
Timing and Subtlety
Most animated gradients fail because they move too fast or use colors that are too far apart. A background is supposed to support the content, not compete with it. Guidelines that keep a moving gradient background calm:
- Slow it down. Full cycles of roughly 10 to 30 seconds feel ambient. Short cycles of a few seconds quickly become busy behind text.
- Use easing.
ease-in-outsoftens the turnaround points, whilelinearcan feel mechanical when the direction reverses. - Choose neighboring hues. Colors that sit close together on the color wheel blend without passing through grey. Indigo, violet and pink move more gracefully than red, green and blue. For more stops, see our guide to creating multi-color CSS gradients.
- Keep lightness similar across stops when text sits on top, so contrast does not swing as colors drift under the copy.
For text over moving color, check contrast against every stop, not only the first frame. The color contrast checker makes it quick to test your text color against each gradient color in turn.
Respect prefers-reduced-motion
Large moving areas can cause discomfort, dizziness or distraction for some people, and operating systems let users request less motion. Honor that request with a media query that stops the animation and leaves a pleasant static frame:
@media (prefers-reduced-motion: reduce) {
.hero {
animation: none;
background-position: 50% 50%;
}
}
Setting a deliberate position matters. Without it, the gradient freezes at the initial value of 0% 0%, the top-left corner of the oversized image, which may not be its most attractive part. For prominent, long-running motion, also consider a visible pause button: WCAG's Pause, Stop, Hide criterion covers moving content that starts automatically, lasts more than five seconds and is shown alongside other content.
Performance: Why Background-Position Repaints
Browsers render in stages: layout, paint and composite. Animations of transform and opacity can usually run on the compositor, often on the GPU, by moving or fading a layer that has already been painted. Animating background-position is different, because the background has to be repainted on every frame. On a button that cost is negligible. On a full-width, full-height hero it means repainting a large area many times per second, which can drain battery and cause jank on lower-powered devices, particularly when the main thread is busy with other work.
The compositor-friendly alternative
For large surfaces, paint the gradient once on an oversized pseudo-element and move that layer with transform:
.hero {
position: relative;
overflow: hidden;
isolation: isolate;
}
.hero::before {
content: "";
position: absolute;
inset: -50%;
z-index: -1;
background: linear-gradient(120deg, #6366f1, #a855f7, #ec4899, #6366f1);
animation: gradient-drift 20s ease-in-out infinite alternate;
}
@keyframes gradient-drift {
from { transform: translate(-20%, -10%); }
to { transform: translate(20%, 10%); }
}
@media (prefers-reduced-motion: reduce) {
.hero::before { animation: none; }
}
The inset: -50% makes the layer twice the size of the hero, so it can drift without exposing an edge, and overflow: hidden crops the excess. isolation: isolate turns the hero into a stacking context, which keeps the negative z-index layer above the hero's own background but behind its content. Since only the transform changes, the browser can move the existing layer instead of repainting it. Browsers typically promote a layer while a transform animation runs, so reserve will-change: transform for cases where profiling shows otherwise. A related trick is to stack two pseudo-elements with different gradients and crossfade them with opacity.
Animating Gradient Angles and Colors With @property
Sometimes you want the gradient itself to change, such as a slowly rotating angle. A plain custom property inside a gradient cannot be interpolated, because the browser does not know its type. Registering it with @property fixes that:
@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.hero {
background: linear-gradient(var(--angle, 0deg), #6366f1, #a855f7, #ec4899);
animation: rotate-gradient 12s linear infinite;
}
@keyframes rotate-gradient {
to { --angle: 360deg; }
}
Because --angle is registered as an angle, the browser calculates every in-between value and the gradient rotates smoothly. The same approach works with syntax: "<color>" to fade individual stops, or "<percentage>" to slide a stop's position. @property is supported in all current major browsers, and the 0deg fallback inside var() keeps a static gradient visible in older ones. Keep in mind that this still changes the background, so like background-position it repaints every frame; use it on smaller elements or keep it slow on large ones. It is also the standard way to build rotating CSS gradient borders, which you can start from with the gradient border generator.
Conclusion
A good animated gradient is slow, built from related colors, paused for people who prefer reduced motion, and implemented with the right technique for its size: background-position for small and medium areas, a transformed pseudo-element for full-screen heroes, and @property when the gradient itself needs to change. Generate the base CSS with the animated gradient generator, design your static stops first with the gradient generator, and read how to create better gradient backgrounds to choose colors that look just as good standing still.
FAQ
Frequently Asked Questions
Can you animate a CSS gradient?
Not directly between two gradient definitions, because background images do not interpolate smoothly. Instead, make the gradient larger than the element and animate background-position, move an oversized gradient layer with transform, or register a custom property with @property so the browser can animate an angle or color inside the gradient.
Why is my CSS gradient animation not moving?
The usual cause is a missing or overwritten background-size. If the gradient is the same size as the element, every background-position percentage resolves to the same spot, so nothing visibly moves. Also check that the background shorthand is not declared after background-size, and that reduced motion is not enabled in your operating system.
Are animated gradient backgrounds bad for performance?
Animating background-position repaints the background on every frame, which is fine for small elements but can be costly on a full-screen hero. For large areas, paint the gradient once on a pseudo-element and animate its transform, which browsers can usually handle on the compositor.
How do I disable a gradient animation for users who prefer reduced motion?
Wrap an override in a prefers-reduced-motion: reduce media query that sets animation to none. Also set a deliberate background-position, such as 50% 50%, so the frozen gradient shows an attractive part of the oversized image instead of its top-left corner.