Scroll velocity skew
Cards lean and stretch in the direction you scroll, based on how fast you are moving, then settle when you stop.
JavaScriptIntermediate
<p class="hint">Scroll fast, then slow down</p>
<div class="col">
<div class="card">One</div><div class="card">Two</div><div class="card">Three</div>
<div class="card">Four</div><div class="card">Five</div><div class="card">Six</div>
</div>
body{display:block;height:auto;padding:30px 0 90px}
.hint{text-align:center;color:#a6938f;font-size:14px;margin-bottom:24px}
.col{width:min(380px,86vw);margin:0 auto;display:grid;gap:30px}
.card{height:170px;border-radius:16px;display:grid;place-items:center;font-size:1.6rem;font-weight:800;color:#270100;will-change:transform}
.card:nth-child(1){background:#f08a5d}.card:nth-child(2){background:#f3d9c8}.card:nth-child(3){background:#e8b04c}
.card:nth-child(4){background:#d98a8a}.card:nth-child(5){background:#b07a6a}.card:nth-child(6){background:#a8523a}
const cards = [...document.querySelectorAll('.card')];
let last = scrollY, skew = 0;
(function loop() {
const velocity = scrollY - last; // pixels moved since the last frame
last = scrollY;
const target = Math.max(-14, Math.min(14, velocity * 0.35));
skew += (target - skew) * 0.12; // ease so it springs back smoothly
if (Math.abs(skew) < 0.01) skew = 0;
cards.forEach(c => {
c.style.transform = skew ? `skewY(${skew}deg) scaleY(${1 + Math.abs(skew) * 0.012})` : '';
});
requestAnimationFrame(loop);
})();
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Scroll velocity skew</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{display:grid;place-items:center;background:#140807;color:#fffffc;font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
body{display:block;place-items:normal;height:auto;padding:30px 0 90px}
.hint{text-align:center;color:#a6938f;font-size:14px;margin-bottom:24px}
.col{width:min(380px,86vw);margin:0 auto;display:grid;gap:30px}
.card{height:170px;border-radius:16px;display:grid;place-items:center;font-size:1.6rem;font-weight:800;color:#270100;will-change:transform}
.card:nth-child(1){background:#f08a5d}.card:nth-child(2){background:#f3d9c8}.card:nth-child(3){background:#e8b04c}
.card:nth-child(4){background:#d98a8a}.card:nth-child(5){background:#b07a6a}.card:nth-child(6){background:#a8523a}
</style></head>
<body>
<p class="hint">Scroll fast, then slow down</p>
<div class="col">
<div class="card">One</div><div class="card">Two</div><div class="card">Three</div>
<div class="card">Four</div><div class="card">Five</div><div class="card">Six</div>
</div>
<script>
const cards = [...document.querySelectorAll('.card')];
let last = scrollY, skew = 0;
(function loop() {
const velocity = scrollY - last; // pixels moved since the last frame
last = scrollY;
const target = Math.max(-14, Math.min(14, velocity * 0.35));
skew += (target - skew) * 0.12; // ease so it springs back smoothly
if (Math.abs(skew) < 0.01) skew = 0;
cards.forEach(c => {
c.style.transform = skew ? `skewY(${skew}deg) scaleY(${1 + Math.abs(skew) * 0.012})` : '';
});
requestAnimationFrame(loop);
})();
</script>
</body></html>
Good for
Image lists and portfolio grids that want a tactile, rubbery feel.
Watch out for
Clamp the skew so fast flicks do not distort content too far, and ease it back to zero every frame.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.