← Scroll

Scroll Velocity Skew

Content leans with the speed of the scroll, as if dragged along, and springs back upright when the scrolling stops. A gentle scroll barely tilts it; a flick leans it right over.

Scroll inside ↓

Scroll

faster

and it

leans

into

the speed

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-scroll-velocity-skew to each element that should lean: headings, images, rows of cards.

Options

Add any of these attributes to the same element.

data-maxThe furthest it leans, in degrees. Default 10.
data-strengthHow strongly speed turns into lean. Default 1; try 2 for a livelier feel.
data-stretchAlso stretch a little in the direction of travel.

Everything in the same scrolling area leans together. 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 () {
    // Scroll Velocity Skew: content leans with the speed of the scroll, as if
    // dragged along, and springs back upright when the scrolling stops. A gentle
    // scroll barely tilts it; a flick leans it right over.
    // Usage: <div data-scroll-velocity-skew>…</div>   (each element that leans)
    // Optional: data-max (largest lean in degrees, default 10), data-strength
    // (how strongly speed turns into lean, default 1), data-stretch (also
    // stretch a little in the direction of travel).
    // Works inside scrolling boxes too: it follows the nearest scrolling parent.
    var DEFAULTS = { max: 10, strength: 1 };

    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;
    }

    // Everything that scrolls together shares one speed reading.
    var groups = [];
    function group(box) {
      for (var i = 0; i < groups.length; i++) if (groups[i].box === box) return groups[i];
      var g = { box: box, items: [], pos: null, time: 0, rest: null };
      var target = box || window;
      function pos() { return box ? box.scrollTop : window.scrollY; }
      target.addEventListener('scroll', function () {
        var now = performance.now(), y = pos();
        if (g.pos !== null && now > g.time) {
          // px per ms, smoothed so one jumpy frame doesn't jerk it.
          var v = (y - g.pos) / Math.max(8, now - g.time);
          g.v = g.v === undefined ? v : g.v * 0.5 + v * 0.5;
          lean(g, g.v);
        }
        g.pos = y;
        g.time = now;
        // No scroll for a moment: stand back up.
        clearTimeout(g.rest);
        g.rest = setTimeout(function () { g.v = 0; lean(g, 0); }, 90);
      }, { passive: true });
      groups.push(g);
      return g;
    }

    function lean(g, v) {
      g.items.forEach(function (it) {
        var deg = Math.max(-it.max, Math.min(it.max, -v * 5 * it.strength));
        it.anim.skewY(deg);
        if (it.stretch) it.anim.scaleY(1 + Math.min(0.12, Math.abs(deg) / it.max * 0.08));
      });
    }

    function init() {
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
      document.querySelectorAll('[data-scroll-velocity-skew]').forEach(function (el) {
        if (el._svs) return;
        el._svs = true;
        var d = el.dataset;
        var stretch = d.stretch !== undefined && d.stretch !== 'false';
        var props = { skewY: { unit: 'deg', duration: 500 }, ease: 'out(3)' };
        if (stretch) props.scaleY = 500;
        group(scroller(el)).items.push({
          anim: anime.createAnimatable(el, props),
          max: d.max !== undefined ? +d.max : DEFAULTS.max,
          strength: d.strength !== undefined ? +d.strength : DEFAULTS.strength,
          stretch: stretch
        });
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h2 data-scroll-velocity-skew>Move fast</h2>
<div class="gallery" data-scroll-velocity-skew>…</div>