Rotate and scale on scroll
Decorative shapes spin and grow or shrink at different rates as they pass through the viewport, adding motion beyond simple up and down.
CSSJavaScriptIntermediate
<div class="space">Scroll down</div>
<div class="field">
<div class="shape sq" data-rot="360" data-scale="1.2"></div>
<div class="shape ci" data-rot="-180" data-scale="-0.5"></div>
<div class="shape tr" data-rot="270" data-scale="0.8"></div>
<div class="shape pl" data-rot="-540" data-scale="0.6">+</div>
</div>
<div class="space">The end</div>
body{display:block;height:auto;overflow-x:hidden}
.space{height:50vh;display:grid;place-items:center;color:#a6938f;font-size:14px}
.field{position:relative;height:160vh}
.shape{position:absolute;will-change:transform}
.sq{left:12%;top:8%;width:90px;height:90px;border:3px solid #f08a5d;border-radius:14px}
.ci{right:14%;top:30%;width:120px;height:120px;border-radius:50%;background:linear-gradient(135deg,#d98a8a,#b07a6a)}
.tr{left:30%;top:54%;width:0;height:0;border-left:50px solid transparent;border-right:50px solid transparent;border-bottom:86px solid #f3d9c8}
.pl{right:20%;top:78%;font-size:80px;font-weight:900;color:#e8b04c;line-height:1}
const shapes = [...document.querySelectorAll('.shape')];
function update() {
const vh = innerHeight;
shapes.forEach(s => {
const r = s.getBoundingClientRect();
if (r.bottom < -100 || r.top > vh + 100) return;
const p = (vh - r.top) / (vh + r.height); // 0 entering at the bottom, 1 leaving at the top
const rot = (p - .5) * s.dataset.rot;
const scale = 1 + (p - .5) * s.dataset.scale;
s.style.transform = `rotate(${rot}deg) scale(${scale})`;
});
}
addEventListener('scroll', () => requestAnimationFrame(update), { passive: true });
update();
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Rotate and scale on scroll</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;overflow-x:hidden}
.space{height:50vh;display:grid;place-items:center;color:#a6938f;font-size:14px}
.field{position:relative;height:160vh}
.shape{position:absolute;will-change:transform}
.sq{left:12%;top:8%;width:90px;height:90px;border:3px solid #f08a5d;border-radius:14px}
.ci{right:14%;top:30%;width:120px;height:120px;border-radius:50%;background:linear-gradient(135deg,#d98a8a,#b07a6a)}
.tr{left:30%;top:54%;width:0;height:0;border-left:50px solid transparent;border-right:50px solid transparent;border-bottom:86px solid #f3d9c8}
.pl{right:20%;top:78%;font-size:80px;font-weight:900;color:#e8b04c;line-height:1}
</style></head>
<body>
<div class="space">Scroll down</div>
<div class="field">
<div class="shape sq" data-rot="360" data-scale="1.2"></div>
<div class="shape ci" data-rot="-180" data-scale="-0.5"></div>
<div class="shape tr" data-rot="270" data-scale="0.8"></div>
<div class="shape pl" data-rot="-540" data-scale="0.6">+</div>
</div>
<div class="space">The end</div>
<script>
const shapes = [...document.querySelectorAll('.shape')];
function update() {
const vh = innerHeight;
shapes.forEach(s => {
const r = s.getBoundingClientRect();
if (r.bottom < -100 || r.top > vh + 100) return;
const p = (vh - r.top) / (vh + r.height); // 0 entering at the bottom, 1 leaving at the top
const rot = (p - .5) * s.dataset.rot;
const scale = 1 + (p - .5) * s.dataset.scale;
s.style.transform = `rotate(${rot}deg) scale(${scale})`;
});
}
addEventListener('scroll', () => requestAnimationFrame(update), { passive: true });
update();
</script>
</body></html>
Good for
Section decoration, illustrated backgrounds and playful brands.
Watch out for
Base the progress on how far the element has travelled through the viewport (0 to 1) so the effect always starts and ends predictably.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.