Copied!
Animated SVG backgrounds

Paper cutout parallax

Eight layered SVG paper cutouts form a tunnel around the Enrichr mark and drift with weighted parallax toward the pointer, with four hole shapes that morph.

← Library

Element · Vanilla JS + SVGUpdated November 6, 2026

SVG backgroundsElementVanilla JS + SVG

Live preview needs JavaScript. The code below works without any dependencies.

Prompt for your AI assistant

Paste this into Claude, Cursor or ChatGPT. The settings update as you move the controls above.

Build "Paper cutout parallax" for my site using the Enrichr Library element at https://enrichr.dev/elements/paper-cutout-parallax.html.

Read the machine-readable spec and source at https://enrichr.dev/elements/paper-cutout-parallax.json (markup, CSS, JavaScript, parameters).
Requirements: vanilla JavaScript and CSS, no new dependencies; keep the behaviour of the source unchanged; respect prefers-reduced-motion; work on touch screens; keep it accessible.

Settings:
- shape: cavern
- motion: idle
- depth: 130
- weight: 0.06
- tilt: 4
- autoSpeed: 0.015
- tagline: Built in layers
- outer: #005236
- inner: #050d0a
- glow: #1a7a4c
- highlight: #e6ff55
- ink: #fbf8f1

Verify: reduced motion shows no movement, it works at 375px wide, there are no console errors, and scrolling stays smooth.

The same spec as JSON, for tools that read it directly: /elements/paper-cutout-parallax.json.

Code

Mount it on an element that contains the markup. It returns an object with update(options) and destroy().

HTML

<div class="ef-pcp">
  <div class="ef-pcp__wrap">
    <div class="ef-pcp__layer ef-pcp__deep" data-speed="0.01"><canvas class="ef-pcp__stars" aria-hidden="true"></canvas></div>
    <div class="ef-pcp__layer" data-speed="0.03"><svg class="ef-pcp__paper" viewBox="0 0 1920 1080" preserveAspectRatio="none" aria-hidden="true" focusable="false"><path class="ef-pcp__p ef-pcp__p--1" fill-rule="evenodd" d="M -50 -50 H 1970 V 1130 H -50 Z M 960 380 C 1070 380, 1150 430, 1170 510 C 1190 590, 1110 670, 980 680 C 850 690, 750 630, 740 550 C 730 470, 850 380, 960 380 Z"/></svg></div>
    <div class="ef-pcp__layer" data-speed="0.06"><svg class="ef-pcp__paper" viewBox="0 0 1920 1080" preserveAspectRatio="none" aria-hidden="true" focusable="false"><path class="ef-pcp__p ef-pcp__p--2" fill-rule="evenodd" d="M -50 -50 H 1970 V 1130 H -50 Z M 960 330 C 1120 320, 1220 400, 1250 500 C 1280 610, 1170 710, 1010 730 C 830 750, 700 670, 680 560 C 660 440, 800 340, 960 330 Z"/></svg></div>
    <div class="ef-pcp__layer" data-speed="0.10"><svg class="ef-pcp__paper" viewBox="0 0 1920 1080" preserveAspectRatio="none" aria-hidden="true" focusable="false"><path class="ef-pcp__p ef-pcp__p--3" fill-rule="evenodd" d="M -50 -50 H 1970 V 1130 H -50 Z M 960 280 C 1180 270, 1310 360, 1340 480 C 1370 620, 1230 760, 1040 780 C 820 800, 650 710, 620 570 C 580 410, 740 290, 960 280 Z"/></svg></div>
    <div class="ef-pcp__layer ef-pcp__text" data-speed="0.14">
      <p class="ef-pcp__word"><svg class="ef-pcp__mark" viewBox="0 0 50 50" aria-hidden="true" focusable="false"><path fill="currentColor" d="m34.213 25.361 c0 2.655 1.381 4.808 3.086 4.808 0.781 0 1.494 -0.452 2.038 -1.198 0.267 -0.366 0.688 -0.589 1.142 -0.589 h0.054 c0.774 0 1.401 0.627 1.401 1.401 v10.65 h-10.651 c-0.774 0 -1.401 0.627 -1.401 1.401 v0.054 c0 0.453 0.222 0.874 0.589 1.141 0.746 0.544 1.198 1.257 1.198 2.038 0 1.705 -2.152 3.086 -4.808 3.086 -2.655 0 -4.808 -1.381 -4.808 -3.086 0 -0.781 0.452 -1.494 1.198 -2.038 0.366 -0.267 0.589 -0.688 0.589 -1.142 v-0.054 c0 -0.774 -0.627 -1.401 -1.401 -1.401 h-10.65 v-10.651 c0 -0.774 -0.627 -1.401 -1.401 -1.401 h-0.054 c-0.453 0 -0.875 0.222 -1.142 0.589 -0.543 0.746 -1.257 1.198 -2.038 1.198 -1.705 0 -3.086 -2.152 -3.086 -4.808 0 -2.655 1.381 -4.808 3.086 -4.808 0.781 0 1.495 0.452 2.038 1.198 0.267 0.366 0.688 0.589 1.141 0.589 h0.054 c0.774 0 1.401 -0.627 1.401 -1.401 v-10.65 h10.65 c0.774 0 1.401 -0.627 1.401 -1.401 v-0.054 c0 -0.453 -0.222 -0.875 -0.589 -1.142 -0.746 -0.543 -1.198 -1.257 -1.198 -2.038 0 -1.705 2.152 -3.086 4.808 -3.086 2.655 0 4.808 1.381 4.808 3.086 0 0.781 -0.452 1.495 -1.198 2.038 -0.366 0.267 -0.589 0.688 -0.589 1.141 v0.054 c0 0.774 0.627 1.401 1.401 1.401 h10.65 v10.65 c0 0.774 -0.627 1.401 -1.401 1.401 h-0.053 c-0.453 0 -0.874 -0.222 -1.141 -0.589 -0.544 -0.746 -1.257 -1.198 -2.038 -1.198 -1.705 0 -3.086 2.152 -3.086 4.808 z"/></svg><span class="ef-pcp__name">Enrichr</span></p>
      <p class="ef-pcp__tag">Built in layers</p>
    </div>
    <div class="ef-pcp__layer" data-speed="0.18"><svg class="ef-pcp__paper" viewBox="0 0 1920 1080" preserveAspectRatio="none" aria-hidden="true" focusable="false"><path class="ef-pcp__p ef-pcp__p--4" fill-rule="evenodd" d="M -50 -50 H 1970 V 1130 H -50 Z M 960 220 C 1240 210, 1400 320, 1440 470 C 1480 640, 1300 810, 1080 840 C 820 870, 580 770, 540 590 C 490 390, 680 230, 960 220 Z"/></svg></div>
    <div class="ef-pcp__layer" data-speed="0.23"><svg class="ef-pcp__paper" viewBox="0 0 1920 1080" preserveAspectRatio="none" aria-hidden="true" focusable="false"><path class="ef-pcp__p ef-pcp__p--5" fill-rule="evenodd" d="M -50 -50 H 1970 V 1130 H -50 Z M 950 160 C 1310 150, 1490 280, 1530 460 C 1580 670, 1380 870, 1120 900 C 810 930, 520 820, 470 610 C 410 360, 590 170, 950 160 Z"/></svg></div>
    <div class="ef-pcp__layer" data-speed="0.30"><svg class="ef-pcp__paper" viewBox="0 0 1920 1080" preserveAspectRatio="none" aria-hidden="true" focusable="false"><path class="ef-pcp__p ef-pcp__p--6" fill-rule="evenodd" d="M -50 -50 H 1970 V 1130 H -50 Z M 940 100 C 1380 90, 1590 230, 1630 450 C 1680 700, 1440 930, 1160 970 C 790 1010, 440 880, 390 620 C 330 320, 500 110, 940 100 Z"/></svg></div>
    <div class="ef-pcp__layer" data-speed="0.38"><svg class="ef-pcp__paper" viewBox="0 0 1920 1080" preserveAspectRatio="none" aria-hidden="true" focusable="false"><path class="ef-pcp__p ef-pcp__p--7" fill-rule="evenodd" d="M -50 -50 H 1970 V 1130 H -50 Z M 930 40 C 1460 30, 1700 190, 1740 440 C 1790 730, 1520 1000, 1200 1040 C 750 1090, 360 940, 300 640 C 230 280, 400 50, 930 40 Z"/></svg></div>
    <div class="ef-pcp__layer" data-speed="0.46"><svg class="ef-pcp__paper" viewBox="0 0 1920 1080" preserveAspectRatio="none" aria-hidden="true" focusable="false"><path class="ef-pcp__p ef-pcp__p--8" fill-rule="evenodd" d="M -50 -50 H 1970 V 1130 H -50 Z M 920 -20 C 1540 -30, 1800 140, 1840 420 C 1890 760, 1600 1060, 1240 1100 C 710 1150, 270 990, 210 650 C 140 230, 300 -10, 920 -20 Z"/></svg></div>
  </div>
</div>

CSS

/* Enrichr Library · https://enrichr.dev */
.ef-pcp { --ef-pcp-u: 12.8px; --ef-pcp-bg: #050d0a; --ef-pcp-glow: #1a7a4c; --ef-pcp-hl: #e6ff55; --ef-pcp-ink: #fbf8f1; --ef-pcp-ink2: #9fb3a8;
  --ef-pcp-l8: #005236; --ef-pcp-l7: #004e33; --ef-pcp-l6: #01472f; --ef-pcp-l5: #013d29; --ef-pcp-l4: #023322; --ef-pcp-l3: #03281b; --ef-pcp-l2: #041d14; --ef-pcp-l1: #04130e;
  position: relative; display: flex; justify-content: center; align-items: center; width: 100%; min-height: 26rem; overflow: hidden; border-radius: 1rem;
  background: var(--ef-pcp-bg); color: var(--ef-pcp-ink); cursor: grab; -webkit-user-select: none; user-select: none; touch-action: pan-y; }
.ef-pcp:active { cursor: grabbing; }
.ef-pcp .ef-pcp__wrap { position: absolute; top: -12%; left: -12%; width: 124%; height: 124%; display: flex; justify-content: center; align-items: center; will-change: transform; perspective: 1000px; }
.ef-pcp .ef-pcp__layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; will-change: transform; transition: transform 0.08s cubic-bezier(0.1, 0.9, 0.2, 1); }
.ef-pcp .ef-pcp__paper { display: block; width: 100%; height: 100%; filter: drop-shadow(0px 18px 25px rgba(0, 0, 0, 0.85)); }
.ef-pcp .ef-pcp__p { transition: d 0.7s cubic-bezier(0.34, 1.25, 0.64, 1); }
.ef-pcp.is-instant .ef-pcp__p { transition: none; }
.ef-pcp .ef-pcp__p--8 { fill: var(--ef-pcp-l8); }
.ef-pcp .ef-pcp__p--7 { fill: var(--ef-pcp-l7); }
.ef-pcp .ef-pcp__p--6 { fill: var(--ef-pcp-l6); }
.ef-pcp .ef-pcp__p--5 { fill: var(--ef-pcp-l5); }
.ef-pcp .ef-pcp__p--4 { fill: var(--ef-pcp-l4); }
.ef-pcp .ef-pcp__p--3 { fill: var(--ef-pcp-l3); }
.ef-pcp .ef-pcp__p--2 { fill: var(--ef-pcp-l2); }
.ef-pcp .ef-pcp__p--1 { fill: var(--ef-pcp-l1); }
.ef-pcp .ef-pcp__deep { z-index: 0; overflow: hidden; background: radial-gradient(circle at center, var(--ef-pcp-glow) 0%, var(--ef-pcp-bg) 80%); }
.ef-pcp .ef-pcp__stars { display: block; width: 100%; height: 100%; opacity: 0.7; }
.ef-pcp .ef-pcp__text { z-index: 5; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; filter: drop-shadow(0 15px 30px rgba(0, 0, 0, 0.9)); }
.ef-pcp .ef-pcp__word { display: flex; align-items: center; gap: 0.12em; margin: 0; padding: 0; opacity: 1; font-family: 'Parkinsans', 'Space Grotesk', sans-serif; font-weight: 700;
  font-size: clamp(44px, calc(var(--ef-pcp-u) * 11), 220px); line-height: 0.85; letter-spacing: -0.02em; color: var(--ef-pcp-ink); }
.ef-pcp .ef-pcp__mark { flex: none; width: 0.82em; height: 0.82em; color: var(--ef-pcp-hl); }
.ef-pcp .ef-pcp__name { padding-bottom: 0.08em; background: linear-gradient(180deg, var(--ef-pcp-ink) 20%, var(--ef-pcp-ink2) 100%); -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent; text-shadow: 0 10px 40px rgba(0, 0, 0, 0.8); }
.ef-pcp .ef-pcp__tag { margin: 0.12em 0 0; padding: 0; opacity: 0.95; font-family: 'Bebas Neue', 'Parkinsans', sans-serif; font-weight: 400;
  font-size: clamp(24px, calc(var(--ef-pcp-u) * 5.5), 110px); line-height: 0.9; letter-spacing: 6px; text-transform: uppercase; color: var(--ef-pcp-hl); }
@media (prefers-reduced-motion: reduce) {
  .ef-pcp .ef-pcp__layer, .ef-pcp .ef-pcp__p { transition: none; }
}

JavaScript

// Enrichr Library · https://enrichr.dev
// Paper cutout parallax: eight SVG sheets of "paper", each a full-size rectangle with a hole cut out (even-odd fill), stacked
// like a tunnel over a starry glow, with the Enrichr mark and wordmark floating in the middle. Every sheet has a drop shadow
// and moves by its own depth factor: the layers chase a target point with a 0.06 lerp per frame (the magnetic weight), shift
// up to 130 px times their factor and tilt up to 4 degrees times their factor in 3D. The target comes from the pointer over
// the element, from a phone's tilt sensor, or from auto motion, which walks an ellipse (cos for x, sin at 0.8x speed for y).
// The hole shape can be cavern, blob, crystal or vortex; switching rewrites each path and the browser morphs it with a CSS
// transition on "d" (0.7 s, slight overshoot) where the two shapes share the same path commands, and swaps it otherwise.
// Stars twinkle by nudging their opacity with a sine each frame.
// Reduced motion shows the scene centred and still, with no twinkle and no morphing. Pauses off screen and in hidden tabs.
// mount(root, { shape ("cavern" | "blob" | "crystal" | "vortex"), motion ("idle" = auto until the pointer is over it,
//               "auto" = always auto, "pointer" = pointer only), depth (max shift, px), weight (follow speed per frame),
//               tilt (max tilt, deg), autoSpeed (radians per frame), tagline, outer / inner (outermost and innermost paper),
//               glow (light in the hole), highlight (mark and tagline), ink (wordmark) })
function mount(root, o) {
  var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  var box = root.querySelector(".ef-pcp");
  var layers = [].slice.call(root.querySelectorAll(".ef-pcp__layer"));
  var speeds = layers.map(function (l) { return parseFloat(l.getAttribute("data-speed")) || 0.1; });
  var paths = [];
  for (var p = 1; p <= 8; p++) paths.push(root.querySelector(".ef-pcp__p--" + p));
  var tag = root.querySelector(".ef-pcp__tag");
  var canvas = root.querySelector(".ef-pcp__stars");
  var ctx = canvas.getContext("2d");
  var RECT = "M -50 -50 H 1970 V 1130 H -50 Z ";
  var SHAPES = {
    cavern: [
      "M 960 380 C 1070 380, 1150 430, 1170 510 C 1190 590, 1110 670, 980 680 C 850 690, 750 630, 740 550 C 730 470, 850 380, 960 380 Z",
      "M 960 330 C 1120 320, 1220 400, 1250 500 C 1280 610, 1170 710, 1010 730 C 830 750, 700 670, 680 560 C 660 440, 800 340, 960 330 Z",
      "M 960 280 C 1180 270, 1310 360, 1340 480 C 1370 620, 1230 760, 1040 780 C 820 800, 650 710, 620 570 C 580 410, 740 290, 960 280 Z",
      "M 960 220 C 1240 210, 1400 320, 1440 470 C 1480 640, 1300 810, 1080 840 C 820 870, 580 770, 540 590 C 490 390, 680 230, 960 220 Z",
      "M 950 160 C 1310 150, 1490 280, 1530 460 C 1580 670, 1380 870, 1120 900 C 810 930, 520 820, 470 610 C 410 360, 590 170, 950 160 Z",
      "M 940 100 C 1380 90, 1590 230, 1630 450 C 1680 700, 1440 930, 1160 970 C 790 1010, 440 880, 390 620 C 330 320, 500 110, 940 100 Z",
      "M 930 40 C 1460 30, 1700 190, 1740 440 C 1790 730, 1520 1000, 1200 1040 C 750 1090, 360 940, 300 640 C 230 280, 400 50, 930 40 Z",
      "M 920 -20 C 1540 -30, 1800 140, 1840 420 C 1890 760, 1600 1060, 1240 1100 C 710 1150, 270 990, 210 650 C 140 230, 300 -10, 920 -20 Z"
    ],
    blob: [
      "M 960 380 C 1040 340, 1150 410, 1140 480 C 1130 540, 1190 610, 1110 660 C 1030 700, 920 640, 860 670 C 790 700, 720 620, 730 550 C 740 470, 800 480, 850 420 C 890 370, 900 400, 960 380 Z",
      "M 960 320 C 1070 270, 1220 370, 1200 470 C 1180 560, 1270 650, 1150 710 C 1030 760, 890 680, 810 720 C 720 760, 640 650, 660 550 C 680 440, 760 450, 820 370 C 880 300, 890 350, 960 320 Z",
      "M 960 260 C 1100 200, 1290 330, 1270 460 C 1240 580, 1340 690, 1200 770 C 1050 830, 870 730, 770 780 C 660 830, 560 690, 580 560 C 610 410, 710 420, 790 320 C 860 230, 880 290, 960 260 Z",
      "M 960 200 C 1140 130, 1370 290, 1340 450 C 1300 600, 1420 730, 1250 820 C 1070 900, 840 780, 720 840 C 590 900, 480 720, 510 560 C 540 380, 670 390, 760 270 C 840 160, 870 230, 960 200 Z",
      "M 960 140 C 1180 60, 1440 250, 1410 440 C 1370 620, 1500 770, 1300 880 C 1090 980, 810 830, 670 900 C 520 970, 400 760, 440 570 C 480 350, 620 360, 730 210 C 820 80, 860 180, 960 140 Z",
      "M 960 80 C 1220 0, 1510 210, 1480 430 C 1440 640, 1580 810, 1350 940 C 1110 1050, 780 880, 620 960 C 450 1040, 320 800, 370 570 C 420 320, 580 330, 700 160 C 800 0, 850 120, 960 80 Z",
      "M 960 20 C 1260 -60, 1580 170, 1550 420 C 1510 660, 1660 850, 1400 1000 C 1130 1130, 750 930, 570 1020 C 380 1110, 240 840, 300 580 C 360 290, 530 300, 670 100 C 780 -80, 840 60, 960 20 Z",
      "M 960 -40 C 1300 -120, 1650 130, 1620 410 C 1580 680, 1740 890, 1450 1060 C 1150 1200, 720 980, 520 1080 C 310 1180, 160 880, 230 590 C 300 260, 480 270, 640 40 C 760 -150, 830 0, 960 -40 Z"
    ],
    crystal: [
      "M 960 400 L 1120 440 L 1160 540 L 1080 640 L 920 660 L 800 580 L 780 480 L 880 410 Z",
      "M 960 350 L 1170 400 L 1220 530 L 1110 670 L 910 700 L 760 600 L 730 460 L 860 370 Z",
      "M 960 300 L 1220 360 L 1280 520 L 1150 700 L 890 740 L 710 630 L 670 440 L 840 320 Z",
      "M 960 250 L 1280 320 L 1340 510 L 1190 740 L 870 780 L 660 660 L 610 420 L 820 280 Z",
      "M 960 200 L 1330 280 L 1400 500 L 1230 780 L 850 830 L 610 690 L 550 400 L 800 230 Z",
      "M 960 150 L 1380 240 L 1460 490 L 1270 820 L 830 870 L 560 720 L 490 380 L 780 180 Z",
      "M 960 100 L 1430 200 L 1520 480 L 1310 860 L 810 920 L 510 750 L 430 360 L 760 130 Z",
      "M 960 50 L 1480 160 L 1580 470 L 1350 900 L 790 970 L 460 780 L 370 340 L 740 80 Z"
    ],
    vortex: [
      "M 960 390 C 1090 390, 1170 450, 1170 540 C 1170 630, 1090 690, 960 690 C 830 690, 750 630, 750 540 C 750 450, 830 390, 960 390 Z",
      "M 960 330 C 1140 320, 1250 410, 1240 540 C 1230 670, 1120 730, 960 740 C 790 740, 680 670, 680 540 C 680 410, 780 340, 960 330 Z",
      "M 960 270 C 1190 250, 1330 370, 1310 540 C 1290 710, 1150 780, 960 790 C 760 790, 620 710, 610 540 C 600 370, 740 280, 960 270 Z",
      "M 960 210 C 1240 180, 1410 330, 1380 540 C 1350 750, 1180 830, 960 840 C 730 840, 550 750, 540 540 C 530 330, 700 230, 960 210 Z",
      "M 960 150 C 1290 110, 1490 290, 1450 540 C 1410 790, 1210 880, 960 890 C 700 890, 480 790, 470 540 C 460 290, 660 180, 960 150 Z",
      "M 960 90 C 1340 40, 1570 250, 1520 540 C 1470 830, 1240 930, 960 940 C 670 940, 410 830, 400 540 C 390 250, 620 130, 960 90 Z",
      "M 960 30 C 1390 -30, 1650 210, 1590 540 C 1530 870, 1270 980, 960 990 C 640 990, 340 870, 330 540 C 320 210, 580 80, 960 30 Z",
      "M 960 -30 C 1440 -100, 1730 170, 1660 540 C 1590 910, 1300 1030, 960 1040 C 610 1040, 270 910, 260 540 C 250 170, 540 30, 960 -30 Z"
    ]
  };
  // share of the outer colour in each sheet, innermost first (the original's midnight ramp)
  var RAMP = [0.085, 0.22, 0.39, 0.56, 0.73, 0.86, 0.95, 1];
  var opts = {}, raf = 0, visible = true, io = null, ro = null, destroyed = false, last = 0, shapeNow = "";
  var W = 1, H = 1, cw = 1, ch = 1, stars = [];
  var currentX = 0, currentY = 0, targetX = 0, targetY = 0, autoAngle = 0, inside = false, tiltAt = -1e9;

  function num(v, d) { v = Number(v); return isFinite(v) ? v : d; }
  function hex(h, d) { var m = /^#?([0-9a-f]{6})$/i.exec(h || ""); var v = parseInt(m ? m[1] : d, 16); return [v >> 16, (v >> 8) & 255, v & 255]; }
  function mix(a, b, t) { return "rgb(" + Math.round(a[0] + (b[0] - a[0]) * t) + "," + Math.round(a[1] + (b[1] - a[1]) * t) + "," + Math.round(a[2] + (b[2] - a[2]) * t) + ")"; }

  function applyShape(key, instant) {
    var list = SHAPES[key] || SHAPES.cavern;
    if (instant) box.classList.add("is-instant");
    for (var i = 0; i < 8; i++) if (paths[i]) paths[i].setAttribute("d", RECT + list[i]);
    if (instant) { void box.offsetWidth; box.classList.remove("is-instant"); }
    shapeNow = key;
  }

  function place() {
    var max = opts.depth, tilt = opts.tilt;
    for (var i = 0; i < layers.length; i++) {
      var s = speeds[i], x = currentX * max * s, y = currentY * max * s;
      layers[i].style.transform = "translate3d(" + x.toFixed(2) + "px, " + y.toFixed(2) + "px, 0px) rotateX(" + (-currentY * tilt * s).toFixed(2) +
        "deg) rotateY(" + (currentX * tilt * s).toFixed(2) + "deg)";
    }
  }

  function initStars() {
    stars = [];
    var count = Math.floor(W * H / 3000), k = cw / W;
    for (var i = 0; i < count; i++) {
      stars.push({ x: Math.random() * cw, y: Math.random() * ch, radius: (Math.random() * 1.5 + 0.5) * k,
        alpha: Math.random() * 0.8 + 0.2, speed: Math.random() * 0.02 + 0.005 });
    }
  }
  function drawStars(f) {
    var t = Date.now();
    ctx.clearRect(0, 0, cw, ch);
    ctx.fillStyle = "#fbf8f1";
    for (var i = 0; i < stars.length; i++) {
      var s = stars[i];
      ctx.globalAlpha = s.alpha;
      ctx.beginPath(); ctx.arc(s.x, s.y, s.radius, 0, Math.PI * 2); ctx.fill();
      if (f) { s.alpha += Math.sin(t * s.speed) * 0.01 * f; if (s.alpha < 0.1) s.alpha = 0.1; if (s.alpha > 0.9) s.alpha = 0.9; }
    }
    ctx.globalAlpha = 1;
  }

  function loop(now) {
    raf = requestAnimationFrame(loop);
    var dt = Math.min(0.05, (now - last) / 1000 || 0); last = now;
    if (!visible || document.hidden) return;
    var f = dt * 60; // the pen counts in 60 fps frames
    var auto = opts.motion === "auto" || (opts.motion === "idle" && !inside && now - tiltAt > 1000);
    if (auto) {
      autoAngle += opts.autoSpeed * f;
      targetX = Math.cos(autoAngle) * 0.75;
      targetY = Math.sin(autoAngle * 0.8) * 0.5;
    }
    var k = 1 - Math.pow(1 - opts.weight, f);
    currentX += (targetX - currentX) * k;
    currentY += (targetY - currentY) * k;
    place();
    drawStars(f);
  }

  function resize() {
    var dpr = Math.min(window.devicePixelRatio || 1, 2);
    W = Math.max(1, box.clientWidth); H = Math.max(1, box.clientHeight);
    cw = Math.max(1, canvas.clientWidth); ch = Math.max(1, canvas.clientHeight);
    canvas.width = Math.round(cw * dpr); canvas.height = Math.round(ch * dpr);
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    box.style.setProperty("--ef-pcp-u", (W / 100).toFixed(2) + "px");
    initStars();
    drawStars(0);
  }

  function update(partial) {
    opts = Object.assign({ shape: "cavern", motion: "idle", depth: 130, weight: 0.06, tilt: 4, autoSpeed: 0.015, tagline: "Built in layers",
      outer: "#005236", inner: "#050d0a", glow: "#1a7a4c", highlight: "#e6ff55", ink: "#fbf8f1" }, opts, partial || {});
    opts.depth = Math.max(0, num(opts.depth, 130)); opts.weight = Math.min(1, Math.max(0.001, num(opts.weight, 0.06)));
    opts.tilt = num(opts.tilt, 4); opts.autoSpeed = num(opts.autoSpeed, 0.015);
    if (!SHAPES[opts.shape]) opts.shape = "cavern";
    if (["idle", "auto", "pointer"].indexOf(opts.motion) < 0) opts.motion = "idle";
    var outer = hex(opts.outer, "005236"), inner = hex(opts.inner, "050d0a");
    for (var i = 0; i < 8; i++) box.style.setProperty("--ef-pcp-l" + (i + 1), mix(inner, outer, RAMP[i]));
    box.style.setProperty("--ef-pcp-bg", opts.inner);
    box.style.setProperty("--ef-pcp-glow", opts.glow);
    box.style.setProperty("--ef-pcp-hl", opts.highlight);
    box.style.setProperty("--ef-pcp-ink", opts.ink);
    box.style.setProperty("--ef-pcp-ink2", mix(hex(opts.ink, "fbf8f1"), outer, 0.42));
    tag.textContent = String(opts.tagline || "");
    tag.style.display = opts.tagline ? "" : "none";
    if (destroyed) return;
    if (opts.shape !== shapeNow) applyShape(opts.shape, reduce || !shapeNow);
    place();
  }

  function onMove(e) {
    inside = true;
    if (opts.motion === "auto") return;
    var b = box.getBoundingClientRect(), cx = b.width / 2, cy = b.height / 2;
    targetX = (e.clientX - b.left - cx) / cx;
    targetY = (e.clientY - b.top - cy) / cy;
  }
  function onTouch(e) { if (e.touches && e.touches[0]) onMove(e.touches[0]); }
  function onLeave() { inside = false; }
  function onTilt(e) {
    if (opts.motion === "auto" || e.gamma === null || e.beta === null || !visible) return;
    tiltAt = performance.now();
    targetX = Math.min(Math.max(e.gamma / 30, -1), 1);
    targetY = Math.min(Math.max(e.beta / 30, -1), 1);
  }

  update(o);
  resize();
  if (!reduce) {
    box.addEventListener("pointermove", onMove);
    box.addEventListener("pointerleave", onLeave);
    box.addEventListener("touchmove", onTouch, { passive: true });
    window.addEventListener("deviceorientation", onTilt);
    if ("IntersectionObserver" in window) { io = new IntersectionObserver(function (e) { visible = e[0].isIntersecting; }); io.observe(box); }
    last = performance.now(); raf = requestAnimationFrame(loop);
  }
  if ("ResizeObserver" in window) { ro = new ResizeObserver(function () { if (!destroyed) resize(); }); ro.observe(box); }
  return {
    update: update,
    destroy: function () {
      destroyed = true; cancelAnimationFrame(raf);
      box.removeEventListener("pointermove", onMove);
      box.removeEventListener("pointerleave", onLeave);
      box.removeEventListener("touchmove", onTouch);
      window.removeEventListener("deviceorientation", onTilt);
      if (io) io.disconnect();
      if (ro) ro.disconnect();
      stars = [];
    }
  };
}

// Usage
var fx = mount(document.querySelector("#my-root"), {"shape":"cavern","motion":"idle","depth":130,"weight":0.06,"tilt":4,"autoSpeed":0.015,"tagline":"Built in layers","outer":"#005236","inner":"#050d0a","glow":"#1a7a4c","highlight":"#e6ff55","ink":"#fbf8f1"});

How it works

Each sheet is a full-size SVG path: a rectangle with a hole cut out of it by the even-odd fill rule, stretched over a box 24% bigger than the element so its edges never show. The eight sheets shade from the outer colour to the inner colour on the same ramp as the original, each with a soft drop shadow, over a radial glow with twinkling stars. A target point comes from the pointer over the element (minus one to one from the centre), a phone's tilt sensor, or auto motion, which walks an ellipse (cosine times 0.75 across, sine at 0.8 times the speed times 0.5 down). Every frame the current point moves 6% of the way to the target, so the layers trail with weight; each layer then shifts by the depth times its own factor (0.01 at the back to 0.46 at the front) and tilts in 3D by up to 4 degrees times that factor. The mark and wordmark float at factor 0.14 above everything. Changing the hole shape rewrites the paths; the browser morphs them with a CSS transition on d where both shapes use the same commands (cavern and vortex) and swaps them otherwise.

Accessibility and performance

The paper layers and stars are hidden from screen readers; the wordmark and tagline are real text. With prefers-reduced-motion the scene shows centred and still: no parallax, no twinkle and no morphing. The motion is decoration and needs no keyboard input; the auto setting shows it without a pointer.

Each layer is its own compositor layer (will-change: transform), so the drop shadows are painted once and only moved each frame. The star canvas is small work: a few hundred dots. The pixel ratio is capped at 2 and the loop pauses off screen or in a hidden tab.

SectionAnimated SVG backgrounds
TechniquesParallax and depth, Pointer interaction, CSS 3D
StackVanilla JS, SVG
Works withReact, Next.js, Vanilla
DependenciesNone
Statustested
On CodePenOpen the live pen on our CodePen account; fork it to experiment.
Inspired byMagnetic Background - SVG Paper Cutout Parallax by Enrichr on CodePen. Original code, written from scratch.

Find this technique on a real site

Want to see this technique in the wild? Check any site with the free tools, then build your own version from this page.

Check which animation library a site uses · Extract a site's design tokens · Browse site breakdowns

Licence and provenance

Free to use in personal and client projects during the preview. Final licence terms will be published here before any paid tier starts; ask before reselling the code as a collection.

This is original code. The technique is a common one that we studied by measuring award-winning sites with Enrichr; no third-party code, shaders, fonts or images are used.

Parallax photo carouselA draggable 3D carousel of gradient photo cards with parallax inner layers, inertia and spring snap, plus arrow keys, buttons and dots. Team card fan heroA "Small team, big results" hero where a fanned row of photo cards drops in, floats, tilts under the pointer and fans out as you scroll away. Depth parallax cardLayered shapes shift by different amounts as the pointer moves, drifting on touch screens.

FAQ

Can I use my own logo or heading?

Yes. Replace the contents of .ef-pcp__text in the markup. It floats on the same layer as the wordmark, so anything you put there moves with the parallax.

Why do some shape changes morph and others jump?

The browser can only blend two paths that use the same drawing commands. Cavern and vortex are both made of four curves, so they morph; blob has six curves and crystal is straight lines, so those swap. The original pen behaves the same way.

Does it work on phones?

Yes. Dragging a finger over it moves the layers, and on phones that report tilt the layers follow the tilt as in the original. Page scrolling still works over it.

How do I make it calmer?

Lower Depth and Tilt, or lower Follow speed so the layers trail further behind the pointer.