CSS and web

CSS animated gradient backgrounds, done properly

Gradients cannot be animated directly, but everything around them can. Four techniques, what each costs the browser, and how to keep motion calm and optional.

GradientlyVerified Gradiently account··4 min read
Cover: Pine Afternoon · GR·6MTX·YD

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.

Three moments of one slow animation. Direction and colour both move, but the palette stays one family.

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.

css
.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 classic animated gradient. Slow durations of 12 to 30 seconds feel ambient rather than busy.

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.

css
@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;
  }
}
Colours and angle animating inside the gradient. Unregistered properties would jump instead of blend.
The two ends of the animation above. The browser blends every frame in between.

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.

css
.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); }
}
A huge conic gradient turning slowly behind the content. The negative inset keeps the corners covered as it rotates.

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.

css
.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; }
}
Day fades into night and back. Only opacity changes, so nothing is repainted.

Choosing a technique

TechniqueBrowser work per frameBest for
Moving background-positionRepaintSmall elements, buttons, text
@property colours and anglesRepaintPrecise colour changes, rotating borders
transform on a layerUsually composited onlyLarge, full-screen backgrounds
opacity crossfadeUsually composited onlySlow mood or palette changes
Repaints are fine on small areas. For large areas, prefer transform and opacity.

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.

css
@media (prefers-reduced-motion: reduce) {
  .hero,
  .hero::before,
  .sky::after {
    animation: none;
  }
}
One rule that stops every technique above. Check that the frame left behind still looks right.

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.

Share this guide

Written by GradientlyVerified Gradiently account

The team behind Gradiently, a design tool built around Marks: living gradients that make everything you design look like yours.

See our profile