Opposite-direction rows
Rows of big words slide sideways in opposite directions as you scroll, creating a weaving, rhythmic parallax.
CSSJavaScriptBeginner
<div class="space">Scroll down</div>
<div class="rows" id="rows">
<div class="row" data-dir="-1">DESIGN MOTION CODE DESIGN</div>
<div class="row" data-dir="1">SHIP LEARN REPEAT SHIP</div>
<div class="row" data-dir="-1">BUILD BETTER BUILD</div>
</div>
<div class="space">The end</div>
body{display:block;height:auto}
.space{height:60vh;display:grid;place-items:center;color:#a6938f;font-size:14px}
.rows{overflow:hidden;padding:30px 0}
.row{white-space:nowrap;font-size:clamp(3.5rem,16vw,7rem);font-weight:900;letter-spacing:-.05em;line-height:1.05;will-change:transform}
.row:nth-child(2){color:transparent;-webkit-text-stroke:2px #f08a5d}
.row:nth-child(3){color:#f3d9c8}
const rows = document.getElementById('rows');
const items = [...rows.querySelectorAll('.row')];
function update() {
const r = rows.getBoundingClientRect();
const fromCenter = r.top + r.height / 2 - innerHeight / 2; // positive below the middle, negative above
items.forEach(it => {
const dir = +it.dataset.dir;
const base = dir > 0 ? -innerWidth * 0.6 : 0; // right-moving rows start off to the left
it.style.transform = `translate3d(${base + fromCenter * 0.9 * dir}px, 0, 0)`;
});
}
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>Opposite-direction rows</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}
.space{height:60vh;display:grid;place-items:center;color:#a6938f;font-size:14px}
.rows{overflow:hidden;padding:30px 0}
.row{white-space:nowrap;font-size:clamp(3.5rem,16vw,7rem);font-weight:900;letter-spacing:-.05em;line-height:1.05;will-change:transform}
.row:nth-child(2){color:transparent;-webkit-text-stroke:2px #f08a5d}
.row:nth-child(3){color:#f3d9c8}
</style></head>
<body>
<div class="space">Scroll down</div>
<div class="rows" id="rows">
<div class="row" data-dir="-1">DESIGN MOTION CODE DESIGN</div>
<div class="row" data-dir="1">SHIP LEARN REPEAT SHIP</div>
<div class="row" data-dir="-1">BUILD BETTER BUILD</div>
</div>
<div class="space">The end</div>
<script>
const rows = document.getElementById('rows');
const items = [...rows.querySelectorAll('.row')];
function update() {
const r = rows.getBoundingClientRect();
const fromCenter = r.top + r.height / 2 - innerHeight / 2; // positive below the middle, negative above
items.forEach(it => {
const dir = +it.dataset.dir;
const base = dir > 0 ? -innerWidth * 0.6 : 0; // right-moving rows start off to the left
it.style.transform = `translate3d(${base + fromCenter * 0.9 * dir}px, 0, 0)`;
});
}
addEventListener('scroll', () => requestAnimationFrame(update), { passive: true });
update();
</script>
</body></html>
Good for
Statement sections, agency homepages and section dividers.
Watch out for
Start the rows offset from each other so they cross over at the middle of the screen. Hide horizontal overflow on the wrapper.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.