Animated CSS Background Generator
Three animated backgrounds with what each costs: one repaints every frame, two are composited, and all of them stop under prefers-reduced-motion.
Enable JavaScript to change the background; the CSS below is for the defaults.
Gradient animates background-position, which repaints. Blobs and stripes animate transform on a pseudo-element, which the compositor handles.
Also the base colour, which is what shows between the moving parts.
Slow is the point. Anything under about eight seconds on a full-width background reads as agitated rather than alive.
Also used for the keyframes name, so two animated backgrounds on a page keep their own.
A hero that shifts
With words on it, because something will be, and an animated background is the easiest way to make text hard to read.
Two blobs, drifting
With words on it, because something will be, and an animated background is the easiest way to make text hard to read.
Stripes on the move
With words on it, because something will be, and an animated background is the easiest way to make text hard to read.
The preview follows prefers-reduced-motion, like the CSS it generates: with that setting on, the movement stops and the background stays.
/* One declaration, and a repaint on every frame: background-position is
not a composited property. Fine on a small element, expensive on a
full-width hero on a phone. */
.hero {
background: linear-gradient(120deg, rgb(30 86 236), rgb(124 58 237), rgb(226 69 107), rgb(30 86 236));
background-size: 400% 400%;
animation: hero-move 15s ease infinite;
}
@keyframes hero-move {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
/* A moving background is what prefers-reduced-motion exists to stop. The
background stays; the movement does not. */
@media (prefers-reduced-motion: reduce) {
.hero,
.hero::before,
.hero::after {
animation: none;
}
}
There are two ways to make a background move, and they cost completely different amounts.
Animating background-position is one declaration and repaints the element on every frame. It works
everywhere, it is what most tutorials show, and on a full-width hero on a mid-range phone it is the thing
making the page feel heavy.
Animating transform on an oversized pseudo-element is composited: the browser moves a layer it has already
painted. It is more markup and it is the version that stays smooth.
Both are generated here, with the cost written into the CSS, because the one that is easier to write is the
one that is more expensive to run. Whichever you pick, the animation is wrapped so prefers-reduced-motion
stops it: a moving background is exactly what that setting is for, and the right response is to stop the
movement rather than remove the background.
How to use
- Pick a technique. Gradient is the one-declaration version; blobs and stripes are the composited ones.
- Set the colours. The last one is also the base, which is what shows between the moving parts.
- Keep the duration slow. Under about eight seconds a full-width background reads as agitated rather than alive.
Example
The cheap one to write:
/* One declaration, and a repaint on every frame: background-position is
not a composited property. Fine on a small element, expensive on a
full-width hero on a phone. */
.hero {
background: linear-gradient(120deg, rgb(30 86 236), rgb(124 58 237), rgb(226 69 107), rgb(30 86 236));
background-size: 400% 400%;
animation: hero-move 15s ease infinite;
}
@keyframes hero-move {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
And the one that stays smooth, where the two blobs run at different durations so they never move as one:
.hero::before {
top: -20%;
left: -10%;
background: rgb(30 86 236);
animation: hero-move 15s ease-in-out infinite;
}
.hero::after {
right: -10%;
bottom: -20%;
background: rgb(124 58 237);
animation: hero-move 21s ease-in-out infinite reverse;
}
@keyframes hero-move {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(12%, 8%) scale(1.15); }
}
Every version ends with the same media query:
@media (prefers-reduced-motion: reduce) {
.hero,
.hero::before,
.hero::after {
animation: none;
}
}
Pitfalls
background-position is not composited. Animating it repaints the element every frame. Acceptable on a
small element, noticeable on a hero, and the first thing to change if a page feels sluggish.
Slow or nothing. Fifteen to thirty seconds for a full-width background. A fast animation behind text is actively unpleasant to read over.
Text on a moving background is hard to read. The contrast changes as it moves, so the worst frame is the one that matters. Both preview stages carry text for that reason.
Reduced motion is not optional. Some people get motion sick from exactly this. Keep the background, drop the movement.
Two identical durations look mechanical. The blobs version uses 1 and 1.4 times the duration so the two shapes never line up, which is what makes it read as organic.
Blur is the expensive part of the blobs version. A 60px blur over a large area costs real paint once; moving it afterwards is cheap. Reduce the blur before reducing the movement.
Battery, not just frame rate. A permanent animation keeps the compositor awake for as long as the page is open, which matters more on a phone than the frame rate does.
Compatibility
@keyframes, animation, transform, gradients and prefers-reduced-motion all work in every current
browser. Older browsers that do not understand the media query simply keep the animation, which is a safe
failure.
The blobs version uses isolation: isolate and a negative z-index on the pseudo-elements, so they stay
behind the content without escaping the element’s stacking context. The stripes version hides its overflow,
since the striped layer is deliberately larger than the element it sits in.
The stylesheet is built by one function, mirrored in PHP for the first paint, with a case file comparing them
on every build across all three kinds and an unknown one. The tests assert that only the gradient version
animates background-position, that every kind carries a reduced-motion block, and that the block stops the
movement rather than hiding the background.
The keyframes are named from the selector, so two animated backgrounds on one page keep their own animation rather than sharing one and fighting over its duration.
Frequently asked questions
Which technique should I use?
Why is my animation stuttering?
Can I use this on body?
Does will-change help?
will-change only if profiling says to.How do I stop it after a while?
animation-iteration-count instead of infinite, or remove the class with JavaScript once it has
played. A background that moves forever is a background competing with your content forever.