GODRICH

9 CSS Background Animation — One Line, Copy-Paste

A css background animation is a decorative layer — a shifting gradient, a drifting grid, blurred shapes — that loops behind real content using only

Auto-plays · click a tile to jump to its section · all nine in one zip

The nine below move from the simplest trick to the most involved one. 01 shifts nothing but background-position under an oversized gradient. 02, 03, and 05 stack several blurred or morphing shapes and move each one on its own path with transform. 04, 07, and 08 tile a repeating pattern and slide it three different ways, so the eye keeps reading motion instead of a fixed image. 06 fakes grain by jumping background-position with steps() instead of easing it. 09 is the odd one out — it only orbits on its own here because the real version follows the cursor through pointermove. The grid above never waits for a click: watch it for a few seconds and every one of the nine has already looped once, sealed inside a stage that doesn't scroll.

01Gradient shift

A gradient is painted at 220% of its own box in both directions, and only background-position slides between two edges of that oversized canvas. The color band appears to flow like water instead of jumping between fixed stops. Reach for it on a hero section or a premium call-to-action button, anywhere a background needs to feel alive without asking the browser to redraw the gradient itself.

background-position200% 200%ease-in-out
.stage::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: linear-gradient(120deg, $stage-ink, $color, $subject-cream, $stage-ink);
  background-size: 220% 220%;
  animation: flow $duration $easing infinite;
}

@keyframes flow {
  0%   { background-position: 0% 50%; }
  50%  { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

02Mesh gradient drift

Three radial-gradient blobs in different colors sit on top of each other, each one blurred and moved along its own translate path with its own animation-delay. The timing never lines up, so the blobs keep re-blending into new mixes instead of pulsing in unison. It reads as a SaaS landing hero or an app onboarding backdrop, the kind of soft moving color field that looks hand-tuned rather than templated.

radial-gradient 레이어transform: translate비동기 주기
.blob {
  position: absolute;
  width: 42%;
  aspect-ratio: 1;
  border-radius: 50%;
  filter: blur(12px);
  opacity: .9;
}

.b1 { background: radial-gradient($color, transparent 65%); top: -8%; left: -6%; animation: drift1 $duration $easing infinite; }
.b2 { background: radial-gradient($subject-cream, transparent 65%); bottom: -12%; right: -4%; animation: drift2 $duration $easing infinite; animation-delay: -.6s; }
.b3 { background: radial-gradient($stage-ink, transparent 65%); top: 28%; right: 22%; opacity: .65; animation: drift3 $duration $easing infinite; animation-delay: -1.2s; }

03Aurora blur

Two or three blurred, semi-transparent color shapes overlap at filter: blur(3px) and move only through transform — translate and rotate together, never blur or opacity. Each shape swings through a loop that looks like layered light rather than a spinning sticker. Staggered animation-delay values keep the three from ever rotating in sync, which is what sells a dark hero background or the ambient lighting behind a premium landing section.

filter: blur반투명 레이어transform 회전
.aurora {
  position: absolute;
  width: 38%;
  aspect-ratio: 1;
  border-radius: 50%;
  filter: blur(3px);
  opacity: 1;
}

.a1 { background: radial-gradient($subject-cream 0%, $subject-cream 25%, transparent 55%); top: -8%; left: 8%; animation: spin1 $duration linear infinite; }

@keyframes spin1 { 0% { transform: translate(0, 0) rotate(0deg); } 50% { transform: translate(56px, 38px) rotate(180deg); } 100% { transform: translate(0, 0) rotate(360deg); } }

04Drifting grid lines

Two repeating-linear-gradient layers, one horizontal and one vertical, draw a plain grid, and a single linear-eased keyframe slides both layers' background-position across exactly one 32px cell. The loop has no visible seam where it restarts. It fits a developer-tool hero or the empty state behind a data dashboard, anywhere a grid should read as infrastructure rather than decoration.

repeating-linear-gradientbackground-position대각선 이동
.stage::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(to right, rgba($color, .35) 1px, transparent 1px),
    linear-gradient(to bottom, rgba($color, .35) 1px, transparent 1px);
  background-size: 32px 32px, 32px 32px;
  animation: gridmove $duration $easing infinite;
}

@keyframes gridmove {
  from { background-position: 0 0, 0 0; }
  to   { background-position: 32px 32px, 32px 32px; }
}

05Blob morph

Three blobs each keep one fixed, hand-picked border-radius value for their whole lifetime, and only opacity and transform (scale, rotate) cross-fade between them on a staggered schedule. The mass looks like it keeps reshaping itself without the browser ever recalculating a border-radius curve mid-frame. It suits an about-page decoration or the illustration behind an empty state, somewhere a blob can breathe without competing with real content.

레이어 크로스페이드transform: scale·rotateopacity
.m1 { border-radius: 42% 58% 63% 37% / 41% 44% 56% 59%; animation: morph1 $duration $easing infinite; }
.m2 { border-radius: 63% 37% 41% 59% / 55% 48% 52% 45%; opacity: 0; animation: morph2 $duration $easing infinite; }
.m3 { border-radius: 35% 65% 55% 45% / 60% 40% 60% 40%; opacity: 0; animation: morph3 $duration $easing infinite; }

@keyframes morph1 {
  0%   { opacity: 1; transform: scale(1) rotate(0deg); }
  28%  { opacity: 1; transform: scale(1.05) rotate(4deg); }
  38%  { opacity: 0; transform: scale(.94) rotate(8deg); }
  92%  { opacity: 0; }
  100% { opacity: 1; transform: scale(1) rotate(0deg); }
}

06Noise grain overlay

A single repeating-conic-gradient tile is painted at just 3 by 3 pixels and tiled edge to edge, then background-position jumps between frames through steps(40) instead of any smoothed easing, so the tiny wedges appear to crawl like real film grain rather than glide. It only ever shifts position, so nothing newer than plain conic-gradient support is needed; animating the gradient's own stop angles directly would call for a registered custom property instead, and @property only reached Chrome 85, Safari 16.4, and Firefox 128. Drop it under a hero image or a premium card at low opacity for texture, not as a whole background on its own.

repeating-conic-gradientsteps()background-position
.stage::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: repeating-conic-gradient(from 0deg, rgba($color, .55) 0deg 90deg, transparent 90deg 180deg);
  background-size: 3px 3px;
  opacity: .35;
  animation: grain $duration $easing infinite;
}

@keyframes grain {
  from { background-position: 0 0; }
  to   { background-position: 117px 69px; }
}

07Drifting dot pattern

A single radial-gradient dot is tiled into a 20px grid, and background-position drifts diagonally at a constant linear pace across exactly one tile's width and height. The pattern loops without ever showing where one tile ends and the next begins. It sits well behind a card grid or a slide-deck background, quiet enough not to compete with whatever is printed on top of it.

radial-gradient 도트background-size 타일대각선 흐름
.stage::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(circle, rgba($color, .6) 1.5px, transparent 1.5px);
  background-size: 20px 20px;
  animation: dotdrift $duration $easing infinite;
}

@keyframes dotdrift {
  from { background-position: 0 0; }
  to   { background-position: 20px 20px; }
}

08Diagonal stripe move

The stripes are drawn straight — a plain repeating-linear-gradient running at 90 degrees — inside a layer that is oversized and then rotated a full 45 degrees with transform. Only after that rotation does background-position slide the pre-rotation layer sideways by one stripe-and-gap width. Building it this way, instead of tilting the gradient itself to 45deg and nudging its position along that angle, is the one real trap in this set — more on why below — and it is what makes the stripes read as marching across a sale banner or a processing-state background instead of standing still.

repeating-linear-gradient45degbackground-position
.stage::before {
  content: "";
  position: absolute;
  inset: -150% -150%;
  transform: rotate(45deg);
  background-image: repeating-linear-gradient(90deg, rgba($color, .18) 0 18px, transparent 18px 36px);
  animation: stripemove $duration $easing infinite;
}

@keyframes stripemove {
  from { background-position: 0 0; }
  to   { background-position: 36px 0; }
}

09Cursor spotlight background

A single blurred radial-gradient circle acts as the spotlight, and in this demo transform: translate alone carries it around a fixed elliptical path. On a live page the same .spot element swaps that scripted path for the real x and y a pointermove listener reads off the cursor, gated behind a matchMedia('(pointer: fine)') check so touch screens never try to track a pointer that isn't there. It turns an interactive landing background or the accent lighting on a dark hero into something that visibly reacts to the one person looking at it.

radial-gradient 스포트라이트transform: translatepointermove 대응
.spot {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 120px;
  height: 120px;
  margin: -60px 0 0 -60px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba($color, .55), transparent 70%);
  filter: blur(6px);
  pointer-events: none;
}

.spot.is-demo { animation: orbit $duration $easing infinite; }

@keyframes orbit {
  0%   { transform: translate(-90px, -40px); }
  25%  { transform: translate(90px, -50px); }
  50%  { transform: translate(70px, 60px); }
  75%  { transform: translate(-80px, 55px); }
  100% { transform: translate(-90px, -40px); }
}

Where it breaks — the trap

Item 08 is the one demo in this set that could not be built the obvious way. The natural instinct is to tilt the repeating-linear-gradient itself to 45deg and then slide background-position along that same diagonal — but a gradient that repeats along an axis and a background-position shift along that identical axis cancel each other out completely. Move it by one full stripe-and-gap and the pattern lands exactly back on itself, so the browser paints something that looks frozen even while the animation is technically running every frame. The fix in the demo is to keep the gradient itself perfectly plain — vertical stripes at 90deg, nothing tilted — and instead rotate the whole oversized layer 45 degrees with transform once, up front. background-position then only has to slide sideways in that layer's own pre-rotation coordinate space, which the 45-degree transform carries along for the ride. The stripes visibly march across the stage the way a sale banner needs them to. Grab the archive with every one of the nine fixes already wired in — the vertical-stripe trick from item 08 included — by typing bc82wnvk into the prompt exactly as it reads here, all lowercase, no extra characters added.

Accessibility

None of the nine goes fully still under prefers-reduced-motion, but each settles on a different frozen frame instead of one blanket rule. 01, 03, 04, 06, 07, and 08 simply stop their animation and hold whatever background-position or transform they were last computing. For a gradient, a grid, or a stripe set, that still reads as a complete, legible background rather than a half-drawn one. 02's three blobs freeze mid-drift in their own separate spots rather than resetting to a single overlapping mass, so the mesh still looks intentional even standing still. 05 goes further and fixes .m2 and .m3 at opacity 0, leaving only the first blob's own border-radius shape visible. Three overlapping static blobs would read as clutter where one clean shape reads as calm. 09 is the outlier. The demo's own automatic orbit stops, but on a live page the reduced-motion query never touches the pointermove listener at all, since that motion is a direct response to where a real cursor already is rather than a loop the page invented on its own. MDN documents prefers-reduced-motion and the operating-system toggles that turn it on.

@media (prefers-reduced-motion: reduce) {
  .stage::before,
  .blob, .aurora,
  .m1, .m2, .m3,
  .spot.is-demo {
    animation: none;
  }
}

Browse the other patterns on the CSS hub, and check the about page for the short version of what ships here and what stays out.

FAQ

Is there a css background animation generator built into this set?

Not in the point-and-click sense — every demo compiles from three SCSS variables, usually $duration, $easing, and $color, so changing those three already covers most of what a generator's sliders would let you drag. Swap the color token, stretch the duration, and the same nine patterns rebuild themselves into a different-looking set without touching a single selector.

Do these css background animation examples loop without a visible seam?

Yes, and that is the actual engineering behind most of them. 01, 04, 06, 07, and 08 all move background-position across exactly one full tile, gradient span, or stripe width, so the frame at 100% lines up pixel-for-pixel with the frame at 0% and the restart is invisible. 02, 03, 05, and 09 loop the same way through their transform paths instead — each keyframe's last stop matches its first — so nothing here relies on a hidden reset or a hard cut you would notice on a slow monitor.

Can I preview one of these css background animation examples without a build tool?

Yes — every pattern compiles from plain SCSS and three variables, so pasting one demo's block into CodePen or any other SCSS playground previews it with no bundler involved. What you get from the archive carries an MIT license, so whichever of the nine you keep — vanilla markup or the react/ folder — is yours to reuse without crediting this site back.

Enter the archive password

The password is inside this article. You will find it as you read.