Auto-scrolling side scroller
A looping landscape that moves on its own, with far layers drifting slowly and near layers racing, like a video game background.
CSSJavaScriptIntermediate
<div class="sun"></div>
<h1>Always moving</h1>
<div class="strip s1" data-n="6" data-h="260,380" data-c="#4e1408"></div>
<div class="strip s2" data-n="9" data-h="150,250" data-c="#3a0804"></div>
<div class="strip s3" data-n="16" data-h="80,170" data-c="#1f0f0e"></div>
<div class="ground"></div>
body{display:block;overflow:hidden;background:linear-gradient(#2a0f0b,#e5603a 80%)}
.sun{position:fixed;left:68%;top:16%;width:90px;height:90px;border-radius:50%;background:#ffd166;box-shadow:0 0 70px #ffd166}
.strip{position:fixed;left:0;bottom:0;width:200%;display:flex;animation:run linear infinite;will-change:transform}
.half{flex:0 0 50%;display:flex;align-items:flex-end}
.half i{flex:none;display:block;clip-path:polygon(50% 0,100% 100%,0 100%)}
.s1{animation-duration:80s}.s2{animation-duration:40s}.s3{animation-duration:18s}
@keyframes run{to{transform:translateX(-50%)}}
.ground{position:fixed;left:0;right:0;bottom:0;height:9%;background:#1f0f0e}
h1{position:fixed;top:12%;left:0;right:0;text-align:center;margin:0;font-size:clamp(2rem,8vw,3.4rem);letter-spacing:-.03em;text-shadow:0 4px 30px rgba(0,0,0,.4)}
@media (prefers-reduced-motion:reduce){.strip{animation:none}}
let seed = 11;
const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
document.querySelectorAll('.strip').forEach(strip => {
const n = +strip.dataset.n, [lo, hi] = strip.dataset.h.split(',').map(Number);
const half = document.createElement('div');
half.className = 'half';
for (let i = 0; i < n; i++) {
const t = document.createElement('i');
t.style.width = 100 / n * 1.5 + '%';
t.style.marginRight = -100 / n * 0.5 + '%';
t.style.height = lo + rnd() * (hi - lo) + 'px';
t.style.background = strip.dataset.c;
half.append(t);
}
strip.append(half, half.cloneNode(true)); // two identical halves = seamless loop
});
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Auto-scrolling side scroller</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;overflow:hidden;background:linear-gradient(#2a0f0b,#e5603a 80%)}
.sun{position:fixed;left:68%;top:16%;width:90px;height:90px;border-radius:50%;background:#ffd166;box-shadow:0 0 70px #ffd166}
.strip{position:fixed;left:0;bottom:0;width:200%;display:flex;animation:run linear infinite;will-change:transform}
.half{flex:0 0 50%;display:flex;align-items:flex-end}
.half i{flex:none;display:block;clip-path:polygon(50% 0,100% 100%,0 100%)}
.s1{animation-duration:80s}.s2{animation-duration:40s}.s3{animation-duration:18s}
@keyframes run{to{transform:translateX(-50%)}}
.ground{position:fixed;left:0;right:0;bottom:0;height:9%;background:#1f0f0e}
h1{position:fixed;top:12%;left:0;right:0;text-align:center;margin:0;font-size:clamp(2rem,8vw,3.4rem);letter-spacing:-.03em;text-shadow:0 4px 30px rgba(0,0,0,.4)}
@media (prefers-reduced-motion:reduce){.strip{animation:none}}
</style></head>
<body>
<div class="sun"></div>
<h1>Always moving</h1>
<div class="strip s1" data-n="6" data-h="260,380" data-c="#4e1408"></div>
<div class="strip s2" data-n="9" data-h="150,250" data-c="#3a0804"></div>
<div class="strip s3" data-n="16" data-h="80,170" data-c="#1f0f0e"></div>
<div class="ground"></div>
<script>
let seed = 11;
const rnd = () => (seed = (seed * 16807) % 2147483647) / 2147483647;
document.querySelectorAll('.strip').forEach(strip => {
const n = +strip.dataset.n, [lo, hi] = strip.dataset.h.split(',').map(Number);
const half = document.createElement('div');
half.className = 'half';
for (let i = 0; i < n; i++) {
const t = document.createElement('i');
t.style.width = 100 / n * 1.5 + '%';
t.style.marginRight = -100 / n * 0.5 + '%';
t.style.height = lo + rnd() * (hi - lo) + 'px';
t.style.background = strip.dataset.c;
half.append(t);
}
strip.append(half, half.cloneNode(true)); // two identical halves = seamless loop
});
</script>
</body></html>
Good for
Hero backgrounds, loading screens and 404 pages. Needs no scrolling at all.
Watch out for
Duplicate each strip and translate by exactly -50% for a seamless loop. Use transform animation, not background-position.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.