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

9 CSS 3D Background Animation Depth Layers

This css 3d background animation set has nine layers below. Layer 02 loses focus with distance, layer 07 loses shadow strength with distance, and scene 09

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

9 CSS 3D Background Animation Layers: What Each One Calculates

All nine layers below run on the same 2D canvas loop: read a distance or pointer value, convert it into an offset, a blur, an opacity, or a tilt, and repaint only the pixels that moved. Layer 01 is the cleanest read of that loop, a set of shapes whose parallax offset scales inversely with a stored depth value. By layer 05 the same loop reads a pointer position instead of a fixed depth to tilt a ring, and layer 09 folds parallax, blur, perspective, tilt, shadow, and scale into one hero scene, the same layering approach that 3D Hover Effect CSS: 9 Pointer-Following Scenes uses for pointer-following cards instead of a background. Among these css background animation examples, the order below moves from a single parallax layer, through blur and drift, into grid perspective, orbit tilt, light falloff, shadow height, and particle scale, before combining all of it into one composite scene.

01Floating Shapes Parallaxing at a Different Speed Per Depth Layer

Each shape carries a virtual depth value, and distanceMultiplier() turns that value into a per-shape offset multiplier so pointer movement drags near shapes further than far ones. stepSim() then eases every shape's position toward that target at an INERTIA_LERP_RATE of 5.2, so the parallax settles instead of snapping.

Floating Shapes Parallaxing at a Different Speed Per Depth Layer
Open in a new tab

01 · Floating Shapes Parallaxing at a Different Speed Per Depth Layer — Measured area 20.586% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

distanceMultiplierINERTIA_LERP_RATEPARALLAX_REF_DISTANCE_PX
  function stepSim(dtMs) {
    var dtS = dtMs / 1000;
    var rate = Math.min(1, INERTIA_LERP_RATE * dtS);
    for (var i = 0; i < shapes.length; i += 1) {
      var s = shapes[i];
      var mult = distanceMultiplier(s.distancePx);
      var tx = pointerNormX * PARALLAX_MAX_OFFSET_PX * mult;
      var ty = pointerNormY * PARALLAX_MAX_OFFSET_PX * mult * 0.6;
      s.curX += (tx - s.curX) * rate;
      s.curY += (ty - s.curY) * rate;
    }
  }

  function drawScene() {
    var sx = cw / CALC_W, sy = ch / CALC_H;
    ctx.clearRect(0, 0, cw, ch);
    for (var i = 0; i < shapes.length; i += 1) {
      var s = shapes[i];
      var alpha = Math.max(0.35, Math.min(1, 1 - s.distancePx / 190));
      var dx = (s.baseX + s.curX) * sx, dy = (s.baseY + s.curY) * sy;

Item 01 repainted 20.6% of the frame at an average intensity of 61.1 across 23/23 measured frames.

02Background Blurring Out of Focus by Simulated Distance

focalDepthAt() sweeps a virtual focus point back and forth on a FOCAL_CYCLE_MS of 4200, and blurForDepth() turns each circle's distance from that focus point into a blur amount capped at a DEPTH_BLUR_MAX_PX of 14. Whichever circles sit closest to the current focus read sharp for that stretch, while the rest soften, the same way a camera racks focus between two subjects.

Background Blurring Out of Focus by Simulated Distance
Open in a new tab

02 · Background Blurring Out of Focus by Simulated Distance — Measured area 16.966% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

blurForDepthDEPTH_BLUR_MAX_PXfocalDepthAt
  function focalDepthAt(t) {
    var f = (Math.sin(t / FOCAL_CYCLE_MS * Math.PI * 2) + 1) / 2;
    return FOCAL_RANGE_NEAR + f * (FOCAL_RANGE_FAR - FOCAL_RANGE_NEAR);
  }

  function blurForDepth(distancePx, focalDepth) {
    var diff = Math.abs(distancePx - focalDepth);
    return Math.max(0, Math.min(DEPTH_BLUR_MAX_PX, diff / 9));
  }

  var elapsed = 0;
  var cw = 480, ch = 300;

  function drawScene(nowMs) {
    var sx = cw / CALC_W, sy = ch / CALC_H;
    var savg = (sx + sy) / 2;
    var focalDepth = focalDepthAt(nowMs);
    ctx.clearRect(0, 0, cw, ch);
    ctx.globalCompositeOperation = 'lighter';
    var order = circles.slice().sort(function (a, b) { return b.distancePx - a.distancePx; });
    for (var i = 0; i < order.length; i += 1) {
      var c = order[i];
      var blurPx = blurForDepth(c.distancePx, focalDepth);

Item 02 repainted only 17.0% of the frame at an average intensity of 15.1 across 15/23 measured frames, the flattest read of the nine because a slow sine-based focus cycle spends long stretches near its extremes.

03Gradient Mesh Blobs Drifting at Different Depth Layers

blobPositionAt() moves each gradient blob along its own sine path sized by a per-blob reachPx, drifting on its own period that stretches longer through each of the MESH_LAYER_COUNT of 5 layers, scaled up from a shared MESH_DRIFT_PERIOD_BASE_MS. drawScene() paints every blob through globalCompositeOperation set to lighter with its own createRadialGradient(), so overlapping blobs brighten instead of simply stacking flat color.

Gradient Mesh Blobs Drifting at Different Depth Layers
Open in a new tab

03 · Gradient Mesh Blobs Drifting at Different Depth Layers — Measured area 52.471% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

blobPositionAtMESH_DRIFT_PERIOD_BASE_MSMESH_LAYER_COUNT
  function blobPositionAt(layer, t) {
    var ax = Math.sin(t / layer.period * Math.PI * 2 + layer.phaseX) * layer.reachPx;
    var ay = Math.cos(t / layer.period * Math.PI * 2.3 + layer.phaseY) * layer.reachPx * 0.7;
    return { x: layer.centerX + ax, y: layer.centerY + ay };
  }

  var elapsed = 0;
  var cw = 480, ch = 300;

  function drawScene(nowMs) {
    var sx = cw / CALC_W, sy = ch / CALC_H;
    var savg = (sx + sy) / 2;
    ctx.clearRect(0, 0, cw, ch);
    ctx.globalCompositeOperation = 'lighter';
    for (var i = 0; i < meshLayers.length; i += 1) {
      var layer = meshLayers[i];
      var pos = blobPositionAt(layer, nowMs);
      var alpha = Math.max(0.3, 0.72 - layer.depthIndex * 0.1);
      var dx = pos.x * sx, dy = pos.y * sy, dr = layer.radius * savg;
      var g = ctx.createRadialGradient(dx, dy, 0, dx, dy, dr);
      g.addColorStop(0, layer.color);
      g.addColorStop(1, 'rgba(0,0,0,0)');
      ctx.globalAlpha = alpha;

Item 03 repainted 52.5% of the frame at an average intensity of 20.9 across 23/23 measured frames, the largest painted area of the nine.

04Perspective Grid Floor Converging Toward the Horizon

projectGridPoint() divides a GRID_PERSPECTIVE_PX of 800 by each row's depth to collapse a flat floor grid toward one horizon point, and nudgeCamHeight() shifts that horizon line as the virtual camera height changes. This is the only one of the nine without a seedRand() call, since its grid rows and horizon sit at fixed positions rather than randomized ones.

Perspective Grid Floor Converging Toward the Horizon
Open in a new tab

04 · Perspective Grid Floor Converging Toward the Horizon — Measured area 23.426% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

projectGridPointGRID_PERSPECTIVE_PXnudgeCamHeight
  function projectGridPoint(xWorld, zWorld) {
    var z = Math.max(4, zWorld);
    var scale = GRID_PERSPECTIVE_PX / (z * 40);
    return {
      x: CALC_W * 0.5 + (xWorld - viewOffsetX) * scale,
      y: HORIZON_Y + camHeight * scale
    };
  }

  var elapsed = 0;
  var scrollZ = 0;
  var cw = 480, ch = 300;

  function drawScene() {
    var sx = cw / CALC_W, sy = ch / CALC_H;
    ctx.clearRect(0, 0, cw, ch);

    ctx.fillStyle = 'rgba(255,140,40,.16)';
    var sunR = 30 + Math.sin(elapsed * 0.0009) * 4;
    ctx.beginPath();
    ctx.arc(CALC_W * 0.5 * sx, HORIZON_Y * sy, sunR * sx, 0, Math.PI * 2);
    ctx.fill();

Item 04 repainted 23.4% of the frame at an average intensity of 39.8 across 23/23 measured frames.

05Orbiting Satellite Ring Tilting to Follow the Pointer

zSortSatellites() reorders the ring's satellites by their computeTransformed() depth every frame, so a satellite swinging behind the ring draws before the ones in front of it. Pointer movement tilts the whole ring by up to an ORBIT_TILT_MAX_DEG of 18, and zSortSatellites() folds that same tilt into each satellite's own orbital angle to get the z-depth it sorts by.

Orbiting Satellite Ring Tilting to Follow the Pointer
Open in a new tab

05 · Orbiting Satellite Ring Tilting to Follow the Pointer — Measured area 10.212% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

zSortSatellitesORBIT_TILT_MAX_DEGcomputeTransformed
  function satelliteAngleAt(sat, t) {
    return sat.baseAngle + t * ANGULAR_SPEED;
  }

  var tiltDeg = 0, azimuthOffset = 0;
  var pointerActive = false;

  function computeTransformed(sat, t) {
    var ang = satelliteAngleAt(sat, t) + azimuthOffset;
    var r = RING_RADIUS * sat.radiusScale;
    var lx = Math.cos(ang) * r;
    var lz = Math.sin(ang) * r;
    var tiltRad = tiltDeg * Math.PI / 180;
    var ry = -lz * Math.sin(tiltRad);
    var rz = lz * Math.cos(tiltRad);
    return { x: lx, y: ry, z: rz };
  }

  function zSortSatellites(list, t) {
    var withZ = list.map(function (sat) {
      var p = computeTransformed(sat, t);
      return { sat: sat, p: p };
    });

Item 05 repainted only 10.2% of the frame at an average intensity of 106.0 across 23/23 measured frames, the smallest painted area of the nine paired with one of its highest intensities.

06Light Shaft Background Fading Layer by Layer

shaftOpacityForDepth() runs an exponential decay against a LIGHT_SHAFT_DECAY of 0.42, so each of the SHAFT_LAYER_COUNT of 7 shafts fades out faster the further its depthFor() value places it from the light source. drawScene() layers every shaft through globalCompositeOperation set to lighter, so crossing shafts brighten instead of overwriting each other.

Light Shaft Background Fading Layer by Layer
Open in a new tab

06 · Light Shaft Background Fading Layer by Layer — Measured area 30.528% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

shaftOpacityForDepthLIGHT_SHAFT_DECAYSHAFT_LAYER_COUNT
  function depthFor(shaft, t) {
    return shaft.distanceBase + Math.sin(t * shaft.swaySpeed + shaft.phase) * shaft.swayRange;
  }

  function shaftOpacityForDepth(depth) {
    var v = Math.exp(-LIGHT_SHAFT_DECAY * depth) * MAX_OPACITY;
    return Math.max(0, Math.min(MAX_OPACITY, v));
  }

  var elapsed = 0;
  var cw = 480, ch = 300;

  function drawScene(t) {
    var sx = cw / CALC_W, sy = ch / CALC_H;
    ctx.clearRect(0, 0, cw, ch);

    ctx.fillStyle = 'rgba(60,40,120,.55)';
    ctx.beginPath();
    ctx.arc(SOURCE_X * sx, SOURCE_Y * sy, 10 * sx, 0, Math.PI * 2);
    ctx.fill();

    ctx.globalCompositeOperation = 'lighter';
    for (var i = 0; i < shafts.length; i += 1) {

Item 06 repainted 30.5% of the frame at an average intensity of 21.6 across 23/23 measured frames.

07Floating Cards Whose Shadow Grows With Their Height

cardBobAt() floats each card on a sine wave capped at a CARD_FLOAT_AMPLITUDE_PX of 22, and shadowForHeight() reads that same height back into a blur radius, an alpha, and a horizontal shadow scale. A card at the top of its bob casts a wide, faint shadow, and the same card at the bottom of its bob casts a tight, dark one from the same formula.

Floating Cards Whose Shadow Grows With Their Height
Open in a new tab

07 · Floating Cards Whose Shadow Grows With Their Height — Measured area 21.405% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

shadowForHeightcardBobAtCARD_FLOAT_AMPLITUDE_PX
  function cardBobAt(card, t) {
    return (Math.sin(t * card.bobSpeed + card.phase) * 0.5 + 0.5) * CARD_FLOAT_AMPLITUDE_PX;
  }

  function shadowForHeight(h) {
    var norm = h / CARD_FLOAT_AMPLITUDE_PX;
    return {
      blur: 3 + norm * 13,
      alpha: 0.4 - norm * 0.26,
      scaleX: 1 + norm * 0.35
    };
  }

  var elapsed = 0;
  var cw = 480, ch = 300;

  function drawScene(t) {
    var sx = cw / CALC_W, sy = ch / CALC_H;
    ctx.clearRect(0, 0, cw, ch);

    for (var i = 0; i < cards.length; i += 1) {
      var card = cards[i];
      var h = cardBobAt(card, t);

Item 07 repainted 21.4% of the frame at an average intensity of 129.0 across 23/23 measured frames, the highest average intensity of the nine.

08Particle Background Scaling Up and Down by Simulated Distance

zAt() advances each particle's depth backward through a PARTICLE_Z_MAX of 120 at its own zAdvanceSpeed(), wrapping back to the far plane once it passes the near one. sizeForDepth() turns that depth into an on-screen radius by dividing a fixed FOCAL of 60 by the particle's current depth, so closer particles render larger.

Particle Background Scaling Up and Down by Simulated Distance
Open in a new tab

08 · Particle Background Scaling Up and Down by Simulated Distance — Measured area 30.615% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

sizeForDepthzAdvanceSpeedPARTICLE_Z_MAX
  function zAdvanceSpeed(p) {
    return 0.012 * p.speedScale;
  }

  function zAt(p, t) {
    var raw = (p.zOffset - t * zAdvanceSpeed(p)) % PARTICLE_Z_MAX;
    if (raw < 0) raw += PARTICLE_Z_MAX;
    return raw + 1;
  }

  function sizeForDepth(z) {
    var v = (FOCAL / z) * 3.4;
    return Math.max(0.8, Math.min(7, v));
  }

  var elapsed = 0;
  var cw = 480, ch = 300;

  function drawScene(t) {
    var sx = cw / CALC_W, sy = ch / CALC_H;
    ctx.clearRect(0, 0, cw, ch);

    for (var i = 0; i < particles.length; i += 1) {

Item 08 repainted 30.6% of the frame at an average intensity of 94.4 across 23/23 measured frames.

09Hero Background Scene Combining Every Depth Layer Technique

The file's own comment lays out six techniques combined into one scene on a shared pointer input: item 01's parallax and item 04's grid-perspective math are locally redefined here as their own distanceMultiplier() and projectGridPoint(), while three HERO_-prefixed constants scale a depth blur like item 02 and a pointer tilt like item 05 on top of their own parallax reach. The remaining two — item 07's height-based shadow alpha and item 08's depth-based scale variance — reappear as their own inline calculations rather than calls to shadowForHeight() or sizeForDepth(), so every one of the six is rewritten locally instead of imported from the other eight files.

Hero Background Scene Combining Every Depth Layer Technique
Open in a new tab

09 · Hero Background Scene Combining Every Depth Layer Technique — Measured area 23.106% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

HERO_ORBIT_TILT_MAX_DEGHERO_PARALLAX_REF_DISTANCE_PXHERO_BLUR_MAX_PX
  function drawScene(t, dtS) {
    var sx = cw / CALC_W, sy = ch / CALC_H;
    var viewOffsetX = nx * 42;
    var tiltDeg = ny * HERO_ORBIT_TILT_MAX_DEG;
    var azimuthOffset = nx * 0.7;

    var targetOffsetX = nx * 14, targetOffsetY = ny * 9;
    var rate = Math.min(1, INERTIA_LERP_RATE * dtS);
    offsetCur += (targetOffsetX - offsetCur) * rate;
    offsetCurY += (targetOffsetY - offsetCurY) * rate;
    var pointerDistancePx = Math.max(20, Math.hypot(offsetCur, offsetCurY) * 6 + 60);
    var mult = distanceMultiplier(pointerDistancePx);

    ctx.clearRect(0, 0, cw, ch);

    ctx.strokeStyle = 'rgba(140,150,230,.5)';
    ctx.lineWidth = 1;
    for (var r = 1; r <= ROW_COUNT; r += 1) {
      var z = r * ROW_SPACING - scrollZ;
      if (z <= 2) continue;
      var pL = projectGridPoint(-COL_COUNT * COL_SPACING * 0.5, z, viewOffsetX);
      var pR = projectGridPoint(COL_COUNT * COL_SPACING * 0.5, z, viewOffsetX);
      ctx.beginPath();

Item 09 repainted 23.1% of the frame at an average intensity of 59.8 across 23/23 measured frames.

Code by Depth Layer Type

All nine of these builds share one calculation space — a fixed CALC_W/CALC_H of 160×100 that fit() rescales to whatever the canvas element measures, so the depth math never has to know the real pixel size of the page it lands on. prefers-reduced-motion is read through matchMedia in all nine, and each one removes its own .is-demo class exactly once on load instead of leaving an always-on autoplay flag running. Five of the nine — items 01, 02, 04, 05, and 09 — also listen for keydown and move the same pointer-driven value a mouse would, so a css 3d animation codepen copy of any of these keeps working from a keyboard alone. Eight of the nine seed their randomness through a local seedRand(); item 04 is the one exception, since its grid rows and horizon sit at fixed positions that never need a random start.

Item 03's overlapping gradient blobs repaint more of the frame than any other layer, item 07's shadow-bearing cards run the highest average intensity, and item 02's slow focal sweep is both the smallest average intensity and the one stretch where only 15 of 23 captured frames moved at all.

Layer Cumulative area Avg. intensity Frames moved
01 Shape parallax 20.6% 61.1 23/23
02 Bokeh blur falloff 17.0% 15.1 15/23
03 Gradient mesh drift 52.5% 20.9 23/23
04 Grid floor perspective 23.4% 39.8 23/23
05 Orbit ring tilt 10.2% 106.0 23/23
06 Light shaft falloff 30.5% 21.6 23/23
07 Card float shadow 21.4% 129.0 23/23
08 Particle scale depth 30.6% 94.4 23/23
09 Hero composite scene 23.1% 59.8 23/23

Where the Value Changes the Depth — Distance, Blur, Shadow, Tilt

Three constants below do most of the depth shaping: DEPTH_BLUR_MAX_PX decides how far out of focus item 02's farthest circle can blur, GRID_PERSPECTIVE_PX decides how sharply item 04's floor grid converges toward the horizon, and ORBIT_TILT_MAX_DEG decides how far item 05's satellite ring can tilt away from flat. None of the three change what gets drawn — they change how much css animation 3d perspective the same background reads as, which is most of what separates a depth-layered scene from one that just moves shapes sideways. 9 CSS 3D Button Effects — Copy-paste Ready tunes a related tilt constant on a single hovered button instead of a whole background layer.

Where It Breaks — Reduced Motion, Narrow Screens, Too Many Layers

prefers-reduced-motion is checked with matchMedia in all nine of these builds — dropping the auto-playing .is-demo class on load rather than disabling the canvas outright, so item 05's ring still tilts and item 09's scene still shifts whenever a pointer actually moves over them. fit() rescales the 160×100 calculation space to whatever the canvas element measures, so a narrow phone width shrinks every layer's on-screen size without changing how the depth math itself runs. Item 03 is the one to watch on a low-end device: five MESH_LAYER_COUNT gradient blobs each draw through globalCompositeOperation set to lighter with their own createRadialGradient(), and that combination is already why it covers 52.5% of the frame — stacking more layers the same way is where a slow device is most likely to drop frames first. The nine index.html/style.scss pairs behind these builds, gt5gztk5, are the fastest way to see exactly how little css 3d background animation code it takes to run all nine of these depth scenes at once.

Three Variants and Accessibility

Variant Value change Feel
Adjust the maximum blur radius 02 DEPTH_BLUR_MAX_PX 14 → 28 Lowering the value keeps even the farthest layer only slightly blurred and the scene looks flat, while raising it spreads the far layer into a heavy blur that makes the depth gap obvious.
Adjust the perspective strength 04 GRID_PERSPECTIVE_PX 800 → 400 Lowering the value makes the floor grid converge sharply and exaggerates the perspective, while raising it flattens the convergence so the scene reads more like a flat backdrop.
Adjust the maximum orbit tilt angle 05 ORBIT_TILT_MAX_DEG 18 → 32 Lowering the value keeps the orbit ring nearly flat even with a lot of pointer movement, while raising it tilts the ring sharply from just a small pointer move.

Every one of the nine checks prefers-reduced-motion once at the top of its own script and answers by dropping the autoplay class rather than turning off individual properties, so a visitor who asked for less motion still sees a layer at rest instead of a blank canvas. Arrow keys work on five of the nine — items 01, 02, 04, 05, and 09 all listen for keydown, nudging a virtual pointer position, a focal point, a camera offset, a tilt angle, or a hero-scene view without a mouse. These three platform pieces sit behind all nine builds: CanvasRenderingContext2D for the drawing surface itself, requestAnimationFrame() for the loop driving it, and the prefers-reduced-motion media query behind the accessibility check above.

What Do CSS 3D Animation Examples Actually Calculate for a Background?

None of these nine load a 3D engine or a WebGL context — every depth effect here is a 2D canvas calculation: a distance-based multiplier, a focal-distance blur, a perspective-divide grid projection, a tilt-driven z-sort, or a straightforward z-to-scale conversion, drawn with plain CanvasRenderingContext2D calls. Eight of the nine seed their starting positions or angles with a local seedRand() so a reload looks the same every time; item 04 is the one exception, since its grid rows and horizon sit at fixed positions and never need a random seed. 3D Globe Website: 9 Canvas Orbit Scenes runs a related no-library canvas calculation, projecting points onto a rotating sphere instead of a layered background.

Frequently Asked Questions About CSS 3D Background Depth Layers

Can I drop one of these nine into a page as a css 3d background animation replacement without any build step?

Yes — every demo is a single self-contained index.html with its script inline, so copying the file and its style.scss companion is the whole setup; nothing here depends on a bundler or an external 3D library.

Why does item 02 show fewer moved frames than the rest?

Item 02's focal point sweeps back and forth on a slow FOCAL_CYCLE_MS of 4200, so only 15 of the 23 measured frames show a large enough blur change to register as movement, while the other eight items all moved on 23 of 23.

Do these nine depth layers need WebGL or a 3D library?

No — every layer here calculates its depth effect with plain 2D canvas math: a distance multiplier, a perspective divide, a z-sort, or a z-to-scale conversion, none of which touch a WebGL context or an imported engine.

Enter the archive password

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