Animated Gradient Backgrounds in CSS: Four Methods Compared | HorizonX
← The Journal

CSS

Animated Gradient Backgrounds in CSS: Four Methods Compared

Four ways to build an animated gradient background in CSS, which ones repaint every frame in Chromium, how to keep corners covered, and when to use a shader.

HorizonX

HorizonX

October 22, 2026 · 7 min read

Animated Gradient Backgrounds in CSS: Four Methods Compared

An animated gradient background is a CSS gradient that shifts over time. The classic way is to animate background-position on an oversized gradient, but that repaints every frame. For a long-running hero background, paint the gradient once on a larger layer and move it with transform, and stop it under reduced motion.

Below are four CSS methods with the trade-offs of each, the results of a paint trace we ran in Chromium on 2026-10-09, and the point where a shader such as the HorizonX Stripe Gradient does a better job than CSS.

The four methods at a glance

Method What moves Repaints every frame? Best for
1. background-position on a large gradient The gradient's position Yes Buttons, small areas, quick prototypes
2. @property angle The gradient's angle Yes Borders, small accents
3. Rotating pseudo-element A pre-painted layer No Full-width hero backgrounds
4. Soft blobs moved with transform Pre-painted blobs No Organic, mesh-like backgrounds

The repaint column comes from our own measurement, described below. web.dev's animations guide gives the general rule behind it: animate transform and opacity, and avoid properties that trigger layout or paint.

Method 1: animate background-position

This is the version you will find in most snippets and in Google's AI overview for this query. Make the gradient three times the size of the element and move its position back and forth.

.gradient-pan {
  background: linear-gradient(120deg, #192c46, #4a6fa5, #c9d6e8, #192c46);
  background-size: 300% 300%;
  animation: pan 12s ease-in-out infinite alternate;
}

@keyframes pan {
  from { background-position: 0% 50%; }
  to { background-position: 100% 50%; }
}

It works everywhere and needs no extra elements. The cost is that background-position is a paint property: the browser redraws the gradient on every frame. On a small button that does not matter. On a full-screen hero that runs for minutes, it keeps the main thread and GPU busy for the whole visit.

Method 2: animate a registered angle with @property

Custom properties normally cannot be interpolated, so animating --angle would jump. Registering it with @property as an <angle> lets the browser tween it, and the gradient follows.

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

.gradient-spin {
  background: linear-gradient(var(--angle), #192c46, #4a6fa5, #c9d6e8);
  animation: spin 16s linear infinite;
}

@keyframes spin {
  to { --angle: 1turn; }
}

@property works in Chrome and Edge 85, Firefox 128 and Safari 16.4 according to MDN's browser-compat data 8.1.5 (checked 2026-10-09; see also caniuse). Firefox 128 shipped on 2024-07-09, so older browsers still in use get a static gradient. The angle changes the gradient itself, so this method also repaints every frame. It shines for animated borders and small accents, not for a full-page background.

Method 3: rotate a pre-painted layer

Paint the gradient once on a pseudo-element that is larger than the section, then rotate that layer. A rotate animation moves pixels that are already painted, which the compositor can handle on its own.

.hero {
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

.gradient-rotate::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: 50%;
  left: 50%;
  width: 150vmax;
  aspect-ratio: 1;
  translate: -50% -50%;
  background: linear-gradient(#192c46 20%, #4a6fa5, #c9d6e8 80%);
  animation: rotate 24s linear infinite;
}

@keyframes rotate {
  to { rotate: 1turn; }
}

The size is the part most snippets get wrong. A rotating square only covers a rectangle at every angle if its side is at least the rectangle's diagonal. width: 200% covers a box up to about 1.73 times taller than it is wide, which fails on narrow, tall mobile sections. 150vmax covers any element that fits in the viewport, because a viewport's diagonal is never more than about 1.42 times its longer side. In our test on 2026-10-09 we paused the rotation at several angles on a red page background and counted red pixels: width: 200% left 13,773 uncovered pixels on a 400 by 900 section, and 150vmax left none at 1000 by 400, 1000 by 1000, 1600 by 700, 400 by 900 and 390 by 844. For a small card, size the layer to the card's diagonal instead, since every composited layer costs memory.

isolation: isolate keeps the negative z-index inside the section, so the layer sits behind the text but in front of the page background.

An oversized square gradient layer rotating behind a smaller rectangular frame, with its corners always reaching past the frame edges

Method 4: soft blobs moved with transform

For a mesh-gradient look, place a few large circles filled with a radial-gradient that fades to transparent, and drift them with transform. The fade replaces filter: blur(), which is costly to animate.

.gradient-blobs {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  background: #192c46;
  color: #fff;
}

.blob {
  position: absolute;
  z-index: -1;
  width: 60%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(closest-side, var(--color), transparent);
  animation: drift var(--duration) ease-in-out infinite alternate;
}

.blob:nth-child(1) { --color: #4a6fa5; --duration: 14s; top: -20%; left: -10%; }
.blob:nth-child(2) { --color: #c9d6e8; --duration: 18s; bottom: -30%; right: -10%; }

@keyframes drift {
  to { transform: translate(25%, 15%) scale(1.1); }
}
<section class="gradient-blobs">
  <span class="blob" aria-hidden="true"></span>
  <span class="blob" aria-hidden="true"></span>
  <h2>Your heading</h2>
</section>

Different durations keep the blobs from moving in lockstep, so the pattern never visibly repeats.

Reduced motion and a pause control

Every method needs the same fallback: stop the animation and keep the static gradient.

@media (prefers-reduced-motion: reduce) {
  .gradient-pan,
  .gradient-spin,
  .gradient-rotate::before,
  .blob {
    animation: none;
  }
}

The prefers-reduced-motion query only helps people who have turned the setting on. WCAG 2.2.2 asks for a way to pause, stop or hide moving content that starts automatically, lasts more than five seconds and sits alongside other content. A small button that toggles animation-play-state: paused on the background covers it. Also check text contrast against the lightest and darkest frames, not only the first one; the guide to grainy backgrounds covers keeping text readable on textured backgrounds.

What we measured

On 2026-10-09 we rendered each method alone in a 1000 by 600 section in Chromium (remote headless browser) and recorded a three-second performance trace after the page settled. Methods 1 and 2 recorded between 362 and 370 paint events per run across two runs. Methods 3 and 4 recorded none, although their animations were running (we read the changing rotate and transform values during the run). With reduced motion set, all four recorded none. One browser and one machine, so read it as a direction, not a benchmark. You can see the same thing in Chrome DevTools with Rendering, Paint flashing turned on.

Four versions of the same gradient panel, each with a small control for a different way to animate it: scaling, moving, rotating or sliding the gradient

When a shader is the better choice

CSS gradients interpolate between color stops in straight lines or circles. If the design calls for broad, flowing folds of color that bend and overlap, CSS gets complicated fast. A WebGL shader computes every pixel from a formula, so the motion can stay fluid without stacking layers.

  • Stripe Gradient renders broad flowing folds of color; you tune scale, flow depth, softness, grain, speed, angle and four colors in the editor.
  • Iridescent Flow gives liquid-metal folds with silver crests and soft reflections.

HorizonX shader pages state that the animation respects the device's reduced-motion preference. The editors export HTML, Svelte or React, and also an MP4 video for places where code cannot run, such as slides or social posts (exports require membership). To decide between CSS, a texture and a shader for a given page, read animated backgrounds with CSS, textures and shaders. For wave shapes rather than moving color, see the CSS wave background guide.

Open the Stripe Gradient editor to compare it with the CSS methods above on your own content.

FAQ

How do I make an animated gradient in CSS?

Set a gradient larger than the element with background-size: 300% 300% and animate background-position. For a full-page background, paint the gradient on an oversized pseudo-element and animate rotate or transform instead, which avoids repainting every frame.

Is there an animated gradient generator?

Several online generators output the background-position version. If you want a shader-based gradient you can tune visually and export as code, the Stripe Gradient editor works that way.

How do I make an animated gradient GIF or video?

CSS cannot export a file. Record the browser with a screen recorder, or use a shader editor that exports video: the HorizonX shader editors export MP4. Prefer MP4 or WebM over GIF, which supports only 256 colors per frame and bands visibly on smooth gradients.