← Scroll

Parallax Layers

The layers of a scene drift at different speeds as you scroll past it: the sun and far hills lag behind while the near ones rush by, giving a flat page real depth. Each layer gets its own speed.

Scroll inside ↓

Parallax

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-parallax-layers to the scene’s container.
  3. Give each layer inside it a data-depth: 0 scrolls with the page, 0.5 at half speed, 1 stays put, and negative values move faster than the page.

Options

Add any of these attributes to the same element.

data-depthOn each layer: how much it lags behind the scroll (see above).
data-smoothOn the container: ease the layers towards their place instead of following the scroll exactly.

Give background layers some extra height, or overflow: hidden on the container, so their edges never come into view as they drift. Works inside scrolling boxes too.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Parallax Layers: the layers of a scene drift at different speeds as you
    // scroll past it, so distant things lag behind and near things rush by,
    // giving a flat page depth.
    // Usage: <div data-parallax-layers>
    //          <img data-depth="0.6" src="sky.png">
    //          <img data-depth="0.3" src="hills.png">
    //          <h2 data-depth="-0.2">Foreground</h2>
    //        </div>
    // data-depth on each layer sets its speed: 0 scrolls with the page, 0.5 at
    // half speed, 1 stays put on screen, and negative values move faster than
    // the page. Layers without data-depth scroll normally.
    // Optional: data-smooth (on the container: ease the layers towards their
    // place instead of following the scroll exactly).
    // Give background layers some extra height (or overflow:hidden on the
    // container) so their edges never come into view as they drift.
    // Works inside scrolling boxes too: it follows the nearest scrolling parent.

    function scroller(el) {
      for (var p = el.parentElement; p && p !== document.body; p = p.parentElement) {
        var o = getComputedStyle(p).overflowY;
        if ((o === 'auto' || o === 'scroll') && p.scrollHeight > p.clientHeight) return p;
      }
      return null;
    }

    function setup(el) {
      var layers = Array.prototype.slice.call(el.querySelectorAll('[data-depth]')).map(function (n) {
        n.style.willChange = 'transform';
        return { el: n, depth: parseFloat(n.dataset.depth) || 0, y: 0 };
      });
      if (!layers.length) return;
      var box = scroller(el);
      var smooth = el.dataset.smooth !== undefined && el.dataset.smooth !== 'false';
      var running = false;

      // How far the scene's centre is from the centre of the view, in px. A
      // layer moved back by depth × that distance travels at (1 - depth) speed.
      function offset() {
        var r = el.getBoundingClientRect();
        var top = 0, h = window.innerHeight;
        if (box) { var b = box.getBoundingClientRect(); top = b.top + box.clientTop; h = box.clientHeight; }
        if (r.bottom < top - h || r.top > top + 2 * h) return null; // well out of view
        return r.top + r.height / 2 - (top + h / 2);
      }

      function frame() {
        running = false;
        var dist = offset();
        if (dist === null) return;
        var moving = false;
        layers.forEach(function (l) {
          var target = -l.depth * dist;
          l.y = smooth ? l.y + (target - l.y) * 0.12 : target;
          if (Math.abs(target - l.y) > 0.3) moving = true;
          else l.y = target;
          l.el.style.transform = 'translate3d(0,' + l.y.toFixed(2) + 'px,0)';
        });
        if (moving) request();
      }

      function request() {
        if (running) return;
        running = true;
        requestAnimationFrame(frame);
      }
      (box || window).addEventListener('scroll', request, { passive: true });
      window.addEventListener('resize', request);
      request();
    }

    function init() {
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
      document.querySelectorAll('[data-parallax-layers]').forEach(function (el) {
        if (el._plx) return;
        el._plx = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div class="hero" data-parallax-layers style="overflow: hidden">
  <img src="sky.png" data-depth="0.7" alt="">
  <img src="hills.png" data-depth="0.4" alt="">
  <h1 data-depth="0.2">Into the hills</h1>
</div>