Depth tunnel fly-through
Scrolling flies the camera forward through a stack of panels in 3D. Each panel rushes toward you, then passes and fades out.
CSSJavaScriptAdvanced
<div class="space"><p>Scroll down<br>to fly forward</p></div>
<section class="track" id="track"><div class="pin">
<div class="panel p0">One</div><div class="panel p1">Two</div><div class="panel p2">Three</div>
<div class="panel p3">Four</div><div class="panel p4">Five</div>
</div></section>
<div class="space"><p>Out the other side.</p></div>
body{display:block;height:auto}
.space{height:50vh;display:grid;place-items:center;color:#a6938f;font-size:14px;text-align:center}
.track{height:420vh}
.pin{position:sticky;top:0;height:100vh;overflow:hidden;perspective:700px;background:radial-gradient(circle at 50% 50%,#2a0f0b,#140807 70%)}
.panel{position:absolute;left:50%;top:50%;width:min(300px,70vw);height:min(200px,40vh);border-radius:16px;display:grid;place-items:center;
font-size:2.2rem;font-weight:800;color:#270100;will-change:transform,opacity}
.p0{background:#f08a5d}.p1{background:#f3d9c8}.p2{background:#e8b04c}.p3{background:#d98a8a}.p4{background:#b07a6a}
const track = document.getElementById('track');
const panels = [...document.querySelectorAll('.panel')];
const gap = 600; // distance between panels in 3D space
function update() {
const max = track.offsetHeight - innerHeight;
const p = Math.min(Math.max(-track.getBoundingClientRect().top / max, 0), 1);
const travel = p * gap * (panels.length); // how far the camera has moved
panels.forEach((el, i) => {
const z = -i * gap + travel; // panel position relative to the camera
const farFade = Math.min(Math.max((z + 1800) / 900, 0), 1); // fade in from the distance
const nearFade = Math.min(Math.max((250 - z) / 250, 0), 1); // fade out as it passes you
el.style.opacity = Math.min(farFade, nearFade);
el.style.transform = `translate(-50%, -50%) translateZ(${z}px) rotate(${(i % 2 ? 1 : -1) * 4}deg)`;
});
}
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>Depth tunnel fly-through</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:50vh;display:grid;place-items:center;color:#a6938f;font-size:14px;text-align:center}
.track{height:420vh}
.pin{position:sticky;top:0;height:100vh;overflow:hidden;perspective:700px;background:radial-gradient(circle at 50% 50%,#2a0f0b,#140807 70%)}
.panel{position:absolute;left:50%;top:50%;width:min(300px,70vw);height:min(200px,40vh);border-radius:16px;display:grid;place-items:center;
font-size:2.2rem;font-weight:800;color:#270100;will-change:transform,opacity}
.p0{background:#f08a5d}.p1{background:#f3d9c8}.p2{background:#e8b04c}.p3{background:#d98a8a}.p4{background:#b07a6a}
</style></head>
<body>
<div class="space"><p>Scroll down<br>to fly forward</p></div>
<section class="track" id="track"><div class="pin">
<div class="panel p0">One</div><div class="panel p1">Two</div><div class="panel p2">Three</div>
<div class="panel p3">Four</div><div class="panel p4">Five</div>
</div></section>
<div class="space"><p>Out the other side.</p></div>
<script>
const track = document.getElementById('track');
const panels = [...document.querySelectorAll('.panel')];
const gap = 600; // distance between panels in 3D space
function update() {
const max = track.offsetHeight - innerHeight;
const p = Math.min(Math.max(-track.getBoundingClientRect().top / max, 0), 1);
const travel = p * gap * (panels.length); // how far the camera has moved
panels.forEach((el, i) => {
const z = -i * gap + travel; // panel position relative to the camera
const farFade = Math.min(Math.max((z + 1800) / 900, 0), 1); // fade in from the distance
const nearFade = Math.min(Math.max((250 - z) / 250, 0), 1); // fade out as it passes you
el.style.opacity = Math.min(farFade, nearFade);
el.style.transform = `translate(-50%, -50%) translateZ(${z}px) rotate(${(i % 2 ? 1 : -1) * 4}deg)`;
});
}
addEventListener('scroll', () => requestAnimationFrame(update), { passive: true });
update();
</script>
</body></html>
Good for
Dramatic storytelling, portfolio intros and product walk-throughs.
Watch out for
Fade panels out before they reach the camera, and keep only a few panels on screen. Heavy 3D on big pages can strain phones.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.