GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Rainbow Gradient Background CSS: 9 Patterns

A rainbow gradient background css, explained: Light, you start out with no color. Item 1 layers three sine waves onto you, item 5 blends you where two blobs

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

None of these nine paint a fixed CSS gradient once and stop — each one runs a small renderField() loop that visits every pixel of a 160×100 buffer once per frame, works out a hue from one or two values (a sine wave, a noise sample, a distance to a blob), converts that hue through hslToRgb(), and writes it straight into an ImageData buffer with createImageData()/putImageData(), which draw() then scales up onto the visible 480×300 canvas. That's why the order below runs from a plain sine calculation (item 1) through noise and distance math (items 2 through 7) to a particle system that skips full-frame repainting entirely (item 8), before item 9 recombines two of the earlier calculations into one hero scene.

01Classic Sine-Wave Plasma

Item 1 sums four Math.sin() waves — across x, across y, diagonally, and radially from the canvas center — into one value that becomes hue, repainting the whole buffer from that sum every frame. PLASMA_SINE_FREQ_X, _Y, and _R are what split the screen into the classic demoscene plasma marbling instead of a flat sweep.

Classic Sine-Wave Plasma
Open in a new tab

01 · Classic Sine-Wave Plasma — Measured area 75.243% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

renderFieldhslToRgbPLASMA_SINE_FREQ_X
  function renderField(t) {
    var cx = CALC_W / 2, cy = CALC_H / 2;
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        var dx = x - cx, dy = y - cy;
        var v = Math.sin(x * PLASMA_SINE_FREQ_X + t * PLASMA_TIME_SPEED)
              + Math.sin(y * PLASMA_SINE_FREQ_Y - t * PLASMA_TIME_SPEED * 0.8)
              + Math.sin((x + y) * PLASMA_SINE_FREQ_R * 0.7 + t * PLASMA_TIME_SPEED * 1.2)
              + Math.sin(Math.sqrt(dx * dx + dy * dy) * PLASMA_SINE_FREQ_R - t * PLASMA_TIME_SPEED * 1.5);
        var hue = (v / 4) * 180 + 180 + t * PLASMA_TIME_SPEED * 40;
        var rgb = hslToRgb(hue, 0.68, 0.55);
        offBuf[idx] = rgb[0]; offBuf[idx + 1] = rgb[1]; offBuf[idx + 2] = rgb[2]; offBuf[idx + 3] = 255;
        idx += 4;
      }

Measured: 75.2% of the frame repaints across the loop, intensity averages 62.4, and 23 of 23 sampled frames moved.

02Noise-Driven Hue Flow

Item 2 samples valueNoise2D() twice per pixel at different scales and blends the two into hue and lightness, so the field drifts like slow clouds instead of item 1's sharp marbling. PLASMA_NOISE_SCALE sets how many pixels one noise cell spans — the variant below stretches it from 40 to 160 to turn small speckles into wide, slow-moving cloud shapes.

Noise-Driven Hue Flow
Open in a new tab

02 · Noise-Driven Hue Flow — Measured area 74.738% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

PLASMA_NOISE_SCALEvalueNoise2DseedRand
  function renderField(t) {
    var tt = t / 1000;
    var ox = tt * PLASMA_FLOW_SPEED, oy = tt * PLASMA_FLOW_SPEED * 0.6;
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        var n1 = valueNoise2D(x / PLASMA_NOISE_SCALE + ox, y / PLASMA_NOISE_SCALE * 0.8 + oy);
        var n2 = valueNoise2D(x / PLASMA_NOISE_SCALE * 2.3 - oy, y / PLASMA_NOISE_SCALE * 2.3 + ox);
        var hue = (n1 * 0.7 + n2 * 0.3) * 360 + tt * 30;
        var light = 0.4 + n2 * 0.2;
        var rgb = hslToRgb(hue, 0.6, light);
        offBuf[idx] = rgb[0]; offBuf[idx + 1] = rgb[1]; offBuf[idx + 2] = rgb[2]; offBuf[idx + 3] = 255;
        idx += 4;
      }
    }

Measured: 74.7% of the frame repaints across the loop, intensity averages 28.7, and 23 of 23 sampled frames moved.

03Pointer-Driven Color Ripple

Item 3 measures every pixel's distance to the pointer — or, with no pointer input yet, to a virtual orbit from virtualPointerOrbit() — and turns that distance into a decaying sine wave that displaces where each pixel samples its own color from and shifts its hue at the same time. PLASMA_RIPPLE_AMP sets how far a pixel gets pushed at the ripple's peak.

Pointer-Driven Color Ripple
Open in a new tab

03 · Pointer-Driven Color Ripple — Measured area 66.851% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

PLASMA_RIPPLE_AMPvirtualPointerOrbitcalcFromEvent
  function renderField(t) {
    var p = pointerActive ? realPointer : (reduce ? { x: -999, y: -999 } : virtualPointerOrbit(t));
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        var dx = x - p.x, dy = y - p.y;
        var dist = Math.sqrt(dx * dx + dy * dy) || 0.0001;
        var wave = Math.sin(dist * PLASMA_RIPPLE_FREQ - t * PLASMA_RIPPLE_SPEED);
        var atten = Math.exp(-dist * PLASMA_RIPPLE_DECAY);
        var disp = wave * atten * PLASMA_RIPPLE_AMP;
        var bx = x + (dx / dist) * disp, by = y + (dy / dist) * disp;
        var hue = bx * 1.6 + by * 1.6;
        var light = 0.48 + wave * atten * 0.16;
        var rgb = hslToRgb(hue, 0.64, light);
        offBuf[idx] = rgb[0]; offBuf[idx + 1] = rgb[1]; offBuf[idx + 2] = rgb[2]; offBuf[idx + 3] = 255;
        idx += 4;
      }
    }

Measured: 66.9% of the frame repaints across the loop, intensity averages 32.1, and 23 of 23 sampled frames moved.

04Radial Hue Burst

Item 4 turns the distance from screen center directly into a hue, then layers a sine wave over that same distance to punch alternating light and dark rings through it. PLASMA_RING_FREQ sets how many rings fit across the canvas and PLASMA_RING_SPEED sets how fast they burst outward.

Radial Hue Burst
Open in a new tab

04 · Radial Hue Burst — Measured area 75.385% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

PLASMA_RING_FREQPLASMA_RING_SPEEDrenderField
  function renderField(t) {
    var cx = CALC_W / 2, cy = CALC_H / 2;
    var tt = t / LOOP_MS;
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        var dx = x - cx, dy = y - cy;
        var dist = Math.sqrt(dx * dx + dy * dy);
        var ringPhase = dist * PLASMA_RING_FREQ - tt * PLASMA_RING_SPEED;
        var hue = ringPhase * 360;
        var light = 0.4 + 0.2 * Math.sin(ringPhase * Math.PI * 6);
        var rgb = hslToRgb(hue, 0.7, light);
        offBuf[idx] = rgb[0]; offBuf[idx + 1] = rgb[1]; offBuf[idx + 2] = rgb[2]; offBuf[idx + 3] = 255;
        idx += 4;
      }
    }
  }

Measured: 75.4% of the frame repaints across the loop, intensity averages 60.9, and 23 of 23 sampled frames moved.

05Metaball Color Blend

Item 5 gives every blob a weight at each pixel — PLASMA_BLOB_RADIUS_PX² / (distance² + PLASMA_BLOB_RADIUS_PX × 2), squared — then blends all the blobs' RGB values by that weight, so two colors only mix where both blobs' weights are still large. nearestBlobIndex() picks which blob a pointer drag moves.

Metaball Color Blend
Open in a new tab

05 · Metaball Color Blend — Measured area 60.035% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

PLASMA_BLOB_RADIUS_PXnearestBlobIndexupdateBlobs
  function renderField(t) {
    updateBlobs(t);
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        var sw = 0, rr = 0, gg = 0, bb = 0;
        for (var k = 0; k < blobs.length; k += 1) {
          var bo = blobs[k];
          var dx = x - bo.x, dy = y - bo.y;
          var d2 = dx * dx + dy * dy;
          var w = (PLASMA_BLOB_RADIUS_PX * PLASMA_BLOB_RADIUS_PX) / (d2 + PLASMA_BLOB_RADIUS_PX * 2);
          w = w * w;
          sw += w; rr += bo.rgb[0] * w; gg += bo.rgb[1] * w; bb += bo.rgb[2] * w;
        }
        offBuf[idx] = rr / sw; offBuf[idx + 1] = gg / sw; offBuf[idx + 2] = bb / sw; offBuf[idx + 3] = 255;
        idx += 4;
      }
    }
  }

Measured: 60.0% of the frame repaints across the loop, intensity averages 18.8, and 22 of 23 sampled frames moved.

06Scanline Chroma Shift

Item 6 computes one horizontal shift and one hue offset per row before it loops that row's pixels — PLASMA_ROW_SHIFT_PX nudges the row's x-sampling sideways and PLASMA_ROW_HUE_STEP staggers its hue from the row above it, the old CRT chroma-bleed look built one row at a time. Because every row shifts along x instead of radially, item 6 is one of the few among these nine that actually reads as a rainbow gradient background horizontal at a glance.

Scanline Chroma Shift
Open in a new tab

06 · Scanline Chroma Shift — Measured area 75.342% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

PLASMA_ROW_HUE_STEPPLASMA_ROW_SHIFT_PXrenderField
  function renderField(t) {
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      var rowShift = Math.sin(y * 0.35 + t * PLASMA_ROW_SPEED) * PLASMA_ROW_SHIFT_PX;
      var rowHue = y * PLASMA_ROW_HUE_STEP + t * PLASMA_ROW_SPEED * 30;
      var rowLight = 0.44 + 0.1 * Math.sin(y * 0.6);
      for (var x = 0; x < CALC_W; x += 1) {
        var xs = x + rowShift;
        var hue = rowHue + xs * 1.2;
        var rgb = hslToRgb(hue, 0.7, rowLight);
        offBuf[idx] = rgb[0]; offBuf[idx + 1] = rgb[1]; offBuf[idx + 2] = rgb[2]; offBuf[idx + 3] = 255;
        idx += 4;
      }
    }

Measured: 75.3% of the frame repaints across the loop, intensity averages 31.6, and 23 of 23 sampled frames moved.

07Posterized Gradient Bands

Item 7 computes a smooth diagonal hue first, then posterizeHue() divides 360° into PLASMA_BAND_COUNT equal steps and snaps every pixel's hue to the middle of its own step, turning a gradient into flat color stripes. A visible range input drives PLASMA_BAND_COUNT live from 3 to 24 bands.

Posterized Gradient Bands
Open in a new tab

07 · Posterized Gradient Bands — Measured area 73.538% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

PLASMA_BAND_COUNTposterizeHuerenderField
  function posterizeHue(raw, bands) {
    var step = 360 / bands;
    return Math.floor(raw / step) * step + step / 2;
  }

  function renderField(t) {
    var diag = t * 0.03;
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        var raw = (x * 1.4 + y * 1.1 + diag) % 360;
        var hue = posterizeHue(raw, PLASMA_BAND_COUNT);
        var rgb = hslToRgb(hue, 0.68, 0.5);
        offBuf[idx] = rgb[0]; offBuf[idx + 1] = rgb[1]; offBuf[idx + 2] = rgb[2]; offBuf[idx + 3] = 255;
        idx += 4;
      }
    }

Measured: 73.5% of the frame repaints across the loop, intensity averages 77.3, and 23 of 23 sampled frames moved.

08Flow-Field Particle Drift

Item 8 skips repainting the whole buffer and instead moves PLASMA_PARTICLE_COUNT particles through a valueNoise2D() direction field — each particle reads the noise at its own position as an angle, steps forward along it, and leaves a small trail behind while the previous frame fades by PLASMA_FADE_ALPHA instead of getting cleared.

Flow-Field Particle Drift
Open in a new tab

08 · Flow-Field Particle Drift — Measured area 30.433% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

PLASMA_PARTICLE_COUNTstepSimvalueNoise2D
  function stepSim(dtMs) {
    octx.fillStyle = 'rgba(12,14,22,' + PLASMA_FADE_ALPHA + ')';
    octx.fillRect(0, 0, CALC_W, CALC_H);
    flowT += dtMs * 0.00035;
    var dtS = dtMs / 1000;
    for (var i = 0; i < particles.length; i += 1) {
      var p = particles[i];
      var ang = valueNoise2D(p.x * PLASMA_FLOW_SCALE + flowT, p.y * PLASMA_FLOW_SCALE) * Math.PI * 4;
      p.x += Math.cos(ang) * PLASMA_SPEED_PX_S * dtS;
      p.y += Math.sin(ang) * PLASMA_SPEED_PX_S * dtS;
      if (p.x < 0) p.x += CALC_W; if (p.x >= CALC_W) p.x -= CALC_W;
      if (p.y < 0) p.y += CALC_H; if (p.y >= CALC_H) p.y -= CALC_H;
      var hue = (ang * 57.3 + p.hueSeed + flowT * 40) % 360;
      octx.fillStyle = hslToRgb(hue, 0.75, 0.58);
      octx.fillRect(p.x - 0.6, p.y - 0.6, 1.6, 1.6);

Measured: 30.4% of the frame repaints across the loop, intensity averages 64.3, and 23 of 23 sampled frames moved.

09Hero Background Scene

Item 9 keeps item 1's four-wave sine sum as a base hue, then blends in drifting color blobs sized by HERO_BLOB_RADIUS_PX through the same squared-weight formula item 5 uses — it doesn't introduce a new calculation, it runs both of the earlier ones together into one hero-sized scene.

Hero Background Scene
Open in a new tab

09 · Hero Background Scene — Measured area 72.383% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

HERO_BLOB_RADIUS_PXrenderFieldhslToRgb
    var idx = 0;
    for (var y = 0; y < CALC_H; y += 1) {
      for (var x = 0; x < CALC_W; x += 1) {
        var v = Math.sin(x * PLASMA_SINE_FREQ_X + tt) +
          Math.sin(y * PLASMA_SINE_FREQ_Y + tt * 1.3) +
          Math.sin((x + y) * PLASMA_SINE_FREQ_R * 0.5 + tt * 0.7) +
          Math.sin(Math.sqrt((x - 80) * (x - 80) + (y - 50) * (y - 50)) * PLASMA_SINE_FREQ_R - tt);
        var baseHue = (v / 4) * 180 + 200 + tt * 8;
        var baseRgb = hslToRgb(baseHue, 0.55, 0.4);

        var sr = baseRgb[0], sg = baseRgb[1], sb = baseRgb[2], sw = 1;
        for (var k = 0; k < blobs.length; k += 1) {
          var dx = x - bcx[k], dy = y - bcy[k];
          var d2 = dx * dx + dy * dy;
          var R2 = HERO_BLOB_RADIUS_PX * HERO_BLOB_RADIUS_PX;
          var w = R2 / (d2 + R2 * 2);
          w = w * w * 3.2;
          sr += blobs[k].rgb[0] * w;
          sg += blobs[k].rgb[1] * w;
          sb += blobs[k].rgb[2] * w;
          sw += w;
        }

Measured: 72.4% of the frame repaints across the loop, intensity averages 28.9, and 23 of 23 sampled frames moved.

Where These Nine Rainbow Gradient Backgrounds Actually Show Up

Five of the nine work fine as a set-and-forget backdrop: item 1's classic marbling and item 9's combined hero scene both suit a landing page hero or loading screen where nobody is expected to touch anything, item 2's cloud-like drift fits a card background or a section divider that shouldn't compete with the text on top of it, item 4's ring bursts read well as an event banner or a notification highlight because the rhythm itself signals "something happened," and item 6's scanline shift fits a retro-concept page or a music site where the CRT reference is the point.

The other four lean interactive. Item 3's ripple only earns its place once a pointer is actually present, so it belongs on an interactive hero or landing page rather than anywhere a visitor can't hover. Item 5's draggable blobs suit a portfolio background or a brand intro where a visitor nudging two colors together is itself the moment of delight. Item 7's live band-count slider is closer to a small tool than a backdrop — a poster-style section or an artwork page where changing the band count is the demonstration. Item 8's particle trails fit a dashboard background or a waiting screen, where motion needs to read as "still working" without a hue anywhere near as strong as item 1's or item 4's.

Nine Plasma Rainbow Fields, Measured Frame by Frame

Item Area % Intensity Frames moved
01 plasma-sine-field-classic 75.2 62.4 23/23
02 plasma-noise-hue-flow 74.7 28.7 23/23
03 plasma-pointer-ripple-distort 66.9 32.1 23/23
04 plasma-radial-hue-burst 75.4 60.9 23/23
05 plasma-metaball-color-blend 60.0 18.8 22/23
06 plasma-scanline-chroma-shift 75.3 31.6 23/23
07 plasma-gradient-band-posterize 73.5 77.3 23/23
08 plasma-flow-particle-drift 30.4 64.3 23/23
09 plasma-hero-background-scene 72.4 28.9 23/23

Item 8 sits far below the rest at 30.4% because it only repaints the small square each particle occupies plus a faint fade layer, never the full buffer the other eight recompute every frame. Item 7 posts the highest intensity, 77.3, because posterizeHue() snaps neighboring pixels to different band centers instead of letting hue change smoothly, which produces bigger per-pixel color jumps than any sine or noise calculation here. Item 5 is the one exception to a clean 23/23: two of its drifting blobs land close enough in color on one sampled frame that the pixel-difference check reads no visible change, so it measures 22 of 23.

Turning Three Values Up to See How Far a Plasma Rainbow Field Can Stretch

Variant Value change Feel
Adjust the noise-scale value 02 PLASMA_NOISE_SCALE 40 → 160 Lowering the value packs the noise tighter into small color speckles, and raising it spreads the noise into large, slow-moving cloud-like color blobs.
Adjust the metaball-radius value 05 PLASMA_BLOB_RADIUS_PX 60 → 200 Shrinking the value means the two blobs must be very close before their colors blend, and growing it makes the color bleed and blend even while they're still far apart.
Adjust the particle-count value 08 PLASMA_PARTICLE_COUNT 80 → 400 Lowering the value leaves the particles sparse so the flow lines look thin, and raising it packs them dense enough that the whole screen reads as a solid flowing wash of color.

Where a Plasma Rainbow Field Breaks Down

Every item here still walks a 160×100 buffer pixel by pixel inside renderField() before draw() scales the result up with drawImage() — cheap at this size, but the same loop against a full 1920×1080 hero section would mean well over a hundred times more Math.sin(), Math.sqrt(), or valueNoise2D() calls every frame, which is where a low-end phone starts dropping frames first on item 5 or item 9, since both sum every blob's weight at every pixel instead of touching each pixel once like items 1, 4, 6, or 7 do. The <span class="pw-inline" id="pw">29beunjp</span> protecting every index.html/style.scss pair here sits in this paragraph on purpose — read this far and you already know which pattern to profile first before you unzip the rest. If you only need one still image instead of the animation, every one of these nine is a plain <canvas> element, so right-click any frame and save it as a rainbow gradient background png — no server-side export required.

Accessibility: What Reduced Motion Freezes, and What It Leaves Working

prefers-reduced-motion: reduce removes the is-demo class and stops the requestAnimationFrame() loop on every item, freezing each one at the midpoint of its own LOOP_MS cycle instead of a blank starting frame, so a reader with motion sensitivity still sees each pattern's typical look rather than nothing. Item 7's range input keeps answering keyboard input under reduced motion exactly as it does otherwise, since input events drive PLASMA_BAND_COUNT rather than the frame loop — the one item here with a real keyboard path. Item 5 behaves a little differently: idle blob drift stops with everything else, but dragging a blob still calls draw() directly from the pointer event, so the color blend stays interactive even with motion reduced, while item 3's ripple has no keyboard equivalent and simply moves off-canvas once reduced motion swaps in a { x: -999, y: -999 } point instead of the pointer. Both putImageData() and requestAnimationFrame() are documented on MDN, along with the prefers-reduced-motion media query itself.

How Is a Rainbow Gradient Background GIF Different From a Static Gradient?

A plain CSS linear-gradient() or a looping rainbow gradient background gif both lock their colors in at export time — every frame of the gif is decided before a visitor ever loads the page. Every pattern above instead recomputes hue from t, a pointer position, or a noise sample live in the browser, so the nine constants named in this post (PLASMA_SINE_FREQ_X, PLASMA_NOISE_SCALE, PLASMA_RIPPLE_AMP, and the rest) stay editable after the fact instead of needing a new export. For nearby canvas patterns built the same value-driven way, HTML5 Canvas Animation: 9 Warp & Droplet Effects covers pixel-level distortion instead of hue math, Canvas Particle Animation: 9 Engine Patterns is the closest relative for item 8's particle trail specifically, and 9 CSS Holographic Card Effects (Copy-Paste) is where a rainbow-like sheen comes from CSS gradients and blend modes instead of a canvas loop.

Questions Worth Answering Before You Copy One of These Nine

Does any of these nine load an image or use a library?

No — every one is generative canvas math. hslToRgb() and, on items 2, 8, and 9, seedRand()-driven noise or blob placement are the only inputs; there's no uploaded image and no external library anywhere in the nine files.

Which pattern costs the most to run at full size?

Item 4 covers the largest share of the frame at 75.4% area, but item 7 is the more expensive one per pixel — its intensity of 77.3 comes from posterizeHue() forcing hard jumps between band colors instead of the smooth transitions items 1 through 6 compute, so it's the first one worth profiling on a large hero section.

Can I make this rainbow gradient background pastel instead of saturated?

Yes. Every item's final color comes from one hslToRgb(hue, S, L) call, so lowering the saturation argument — item 1 currently passes 0.68, for instance — toward roughly 0.3 and raising lightness toward 0.75 softens the exact same hue math into pastel tones without touching any loop, frequency, or radius value.

Enter the archive password

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