Copied!
Full page

Parallax pattern sections

Full-height sections whose patterned backgrounds drift slower than the page, so each headline floats over a deeper layer.

← Library

Element · Vanilla JS + CSSUpdated November 6, 2026

HeroElementVanilla JS + CSS

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 "Parallax pattern sections" for my site using the Enrichr Library element at https://enrichr.dev/elements/parallax-pattern-sections.html.

Read the machine-readable spec and source at https://enrichr.dev/elements/parallax-pattern-sections.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:
- titles: Parallax sections, Patterns drift slower, Headlines float above, Keep scrolling
- pattern: mixed
- depth: 25
- height: 360
- accent: #e6ff55
- background: #0c1e18

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/parallax-pattern-sections.json.

Code

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

HTML

<div class="ef-pps"></div>

CSS

/* Enrichr Library · https://enrichr.dev */
.ef-pps { --ef-pps-bg: #0c1e18; --ef-pps-acc: #e6ff55; --ef-pps-mid: color-mix(in srgb, var(--ef-pps-bg) 55%, #005236); width: 100%; font-family: "Helvetica Neue", Arial, "Segoe UI", system-ui, sans-serif; background: var(--ef-pps-bg); container-type: inline-size; }
.ef-pps__sec { position: relative; height: var(--ef-pps-h, 360px); display: flex; align-items: center; justify-content: center; overflow: hidden; isolation: isolate; }
.ef-pps__bg { position: absolute; left: 0; right: 0; top: calc(var(--ef-pps-k, 80px) * -1); bottom: calc(var(--ef-pps-k, 80px) * -1); z-index: -1; will-change: transform; background-color: var(--ef-pps-mid); }
.ef-pps__bg--dots { background-image: radial-gradient(var(--ef-pps-acc) 18%, transparent 21%); background-size: 44px 44px; }
.ef-pps__bg--stripes { background-color: var(--ef-pps-bg); background-image: repeating-linear-gradient(45deg, var(--ef-pps-acc) 0 8px, transparent 8px 48px); }
.ef-pps__bg--grid { background-image: linear-gradient(color-mix(in srgb, var(--ef-pps-acc) 70%, #fff) 3px, transparent 3px), linear-gradient(90deg, color-mix(in srgb, var(--ef-pps-acc) 70%, #fff) 3px, transparent 3px); background-size: 64px 64px; }
.ef-pps__bg--checker { background-color: var(--ef-pps-bg); background-image: conic-gradient(var(--ef-pps-mid) 25%, transparent 0 50%, var(--ef-pps-mid) 0 75%, transparent 0); background-size: 72px 72px; }
.ef-pps__bg--rings { background-color: var(--ef-pps-bg); background-image: repeating-radial-gradient(circle at 50% 50%, var(--ef-pps-acc) 0 4px, transparent 4px 34px); }
.ef-pps__copy { position: relative; display: flex; flex-direction: column; align-items: center; gap: 0.6rem; padding: 0 1rem; text-align: center; }
.ef-pps__n { font: 600 0.72rem/1 ui-monospace, "SFMono-Regular", Menlo, monospace; letter-spacing: 0.12em; color: var(--ef-pps-bg); background: var(--ef-pps-acc); padding: 0.35rem 0.6rem; border-radius: 999px; }
.ef-pps__t { margin: 0; padding: 0.06em 0.28em 0; font-size: clamp(1.6rem, 7.5cqi, 5.5rem); font-weight: 900; line-height: 0.95; letter-spacing: -0.03em; text-transform: uppercase; color: var(--ef-pps-acc); background: var(--ef-pps-bg); box-decoration-break: clone; -webkit-box-decoration-break: clone; }
@media (prefers-reduced-motion: reduce) { .ef-pps__bg { transform: none !important; } }

JavaScript

// Enrichr Library · https://enrichr.dev
// Parallax pattern sections: full-height sections whose patterned backgrounds drift slower than the page, so each headline
// seems to float over a deeper layer. The pattern layer is taller than its section and is moved with translate3d by how far
// the section has travelled through the viewport. Scroll events (from the page or any scrolling parent) schedule one frame;
// nothing runs while the page is still. Patterns are CSS gradients, so there are no images to load.
// mount(root, { titles (comma separated), pattern ("mixed", "dots", "stripes", "grid", "checker", "rings"),
//               depth (0-60, % of the section height the background lags), height (section height in px), accent, background })
function mount(root, o) {
  var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  var box = root.querySelector(".ef-pps");
  var PATTERNS = ["dots", "stripes", "grid", "checker", "rings"];
  var opts = {}, secs = [], raf = 0, visible = true, io = null, K = 0;

  function build() {
    box.textContent = "";
    secs = [];
    var titles = String(opts.titles).split(",").map(function (t) { return t.trim(); }).filter(Boolean).slice(0, 8);
    if (!titles.length) titles = ["Scroll down"];
    titles.forEach(function (t, i) {
      var s = document.createElement("section");
      s.className = "ef-pps__sec";
      s.setAttribute("aria-label", t);
      var bg = document.createElement("div");
      var p = opts.pattern === "mixed" ? PATTERNS[i % PATTERNS.length] : opts.pattern;
      bg.className = "ef-pps__bg ef-pps__bg--" + (PATTERNS.indexOf(p) < 0 ? "dots" : p);
      bg.setAttribute("aria-hidden", "true");
      var copy = document.createElement("div"); copy.className = "ef-pps__copy";
      var n = document.createElement("span"); n.className = "ef-pps__n"; n.setAttribute("aria-hidden", "true");
      n.textContent = String(i + 1).padStart(2, "0") + " / " + String(titles.length).padStart(2, "0");
      var h = document.createElement("h3"); h.className = "ef-pps__t"; h.textContent = t;
      copy.appendChild(n); copy.appendChild(h);
      s.appendChild(bg); s.appendChild(copy);
      box.appendChild(s);
      secs.push({ el: s, bg: bg, last: null });
    });
  }
  function draw() {
    raf = 0;
    if (!visible || reduce || !K) return;
    var vh = window.innerHeight || document.documentElement.clientHeight || 800;
    secs.forEach(function (s) {
      var r = s.el.getBoundingClientRect();
      if (r.bottom < -K || r.top > vh + K) return;
      // 0 when the section's top meets the bottom of the screen, 1 when its bottom leaves the top.
      var p = Math.min(1, Math.max(0, (vh - r.top) / (vh + r.height)));
      var y = Math.round((p - 0.5) * 2 * K * 10) / 10;
      if (y !== s.last) { s.last = y; s.bg.style.transform = "translate3d(0," + y + "px,0)"; }
    });
  }
  function kick() { if (!raf) raf = requestAnimationFrame(draw); }

  function update(next) {
    opts = Object.assign({ titles: "Parallax sections, Patterns drift slower, Headlines float above, Keep scrolling", pattern: "mixed", depth: 25, height: 360, accent: "#e6ff55", background: "#0c1e18" }, opts, next || {});
    opts.depth = Math.min(60, Math.max(0, Number(opts.depth) || 0));
    opts.height = Math.max(160, Number(opts.height) || 360);
    K = Math.round(opts.height * opts.depth / 100);
    box.style.setProperty("--ef-pps-h", opts.height + "px");
    box.style.setProperty("--ef-pps-k", K + "px");
    box.style.setProperty("--ef-pps-acc", opts.accent);
    box.style.setProperty("--ef-pps-bg", opts.background);
    build();
    kick();
  }
  update(o);
  if (!reduce) {
    window.addEventListener("scroll", kick, { passive: true, capture: true });
    window.addEventListener("resize", kick);
    if ("IntersectionObserver" in window) { io = new IntersectionObserver(function (e) { visible = e[0].isIntersecting; if (visible) kick(); }, { rootMargin: "120px 0px" }); io.observe(box); }
  }
  return {
    update: update,
    destroy: function () {
      if (raf) cancelAnimationFrame(raf); raf = 0;
      if (io) io.disconnect();
      window.removeEventListener("scroll", kick, { capture: true });
      window.removeEventListener("resize", kick);
    }
  };
}

// Usage
var fx = mount(document.querySelector("#my-root"), {"titles":"Parallax sections, Patterns drift slower, Headlines float above, Keep scrolling","pattern":"mixed","depth":25,"height":360,"accent":"#e6ff55","background":"#0c1e18"});

How it works

Each section holds a pattern layer that is taller than the section by the depth amount above and below. On every scroll, from the page or any scrolling parent, one animation frame reads each visible section's position and works out how far it has travelled through the viewport: 0 as its top meets the bottom of the screen, 1 as its bottom leaves the top. The layer is shifted with translate3d from minus to plus the depth across that journey, so it moves slower than the page and never shows an edge. The patterns are CSS gradients: dots, diagonal stripes, a grid, a checkerboard and rings.

Accessibility and performance

Each section is a labelled region with a real heading, and the patterns are decorative and hidden from assistive technology. Headlines sit on a solid plate so they stay readable over any pattern. With prefers-reduced-motion the backgrounds stay still and no scroll work is done.

Scroll events only schedule a frame, so nothing runs while the page is still, and work stops when the block is off screen. Each frame reads one bounding box per visible section and writes one transform, which the browser composites without layout. No images are loaded.

SectionFull page
TechniquesScroll-driven, Parallax and depth
StackVanilla JS, CSS
Works withReact, Next.js, Vanilla
DependenciesNone
Statustested
Inspired bySimple parallax sections - ScrollTrigger by GSAP 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.

Layered parallax sceneA seeded landscape of hill ridges, a sun and a headline tucked behind the front hill, each moving at its own rate as you scroll. Word read highlightWords in a paragraph light up one by one as it scrolls through the viewport. Depth parallax cardLayered shapes shift by different amounts as the pointer moves, drifting on touch screens.

FAQ

Why move a layer instead of background-position?

A transform on its own layer is handled by the compositor, while changing background-position repaints the section every frame. The layer is made taller than the section so it can slide without showing an edge.

Can I use photos instead of patterns?

Yes. Give .ef-pps__bg a background-image with background-size: cover. Keep the depth moderate, 20 to 30, so the photo is not cropped too hard.

Does it work inside a scrolling panel?

Yes. It listens for scroll events in the capture phase, so a scrolling parent drives it as well as the page.