Scroll snap sections
Full-screen sections that lock into place as you scroll, with content that animates in on arrival.
CSSJavaScriptIntermediate
<section><div><h2>One</h2><p>Scroll to snap to the next screen</p></div></section>
<section><div><h2>Two</h2><p>Text animates in on arrival</p></div></section>
<section><div><h2>Three</h2><p>Each section is a full screen</p></div></section>
<section><div><h2>Four</h2><p>The end</p></div></section>
html{scroll-snap-type:y mandatory}
body{display:block;height:auto}
section{height:100vh;scroll-snap-align:start;display:grid;place-items:center;text-align:center;padding:20px}
section:nth-child(1){background:#140807}section:nth-child(2){background:#2a0f0b}
section:nth-child(3){background:#1f1210}section:nth-child(4){background:#2f1512}
h2{margin:0;font-size:clamp(2.2rem,9vw,4rem);letter-spacing:-.03em;opacity:0;transform:translateY(40px) scale(.96);
transition:opacity .7s cubic-bezier(.2,.8,.2,1),transform .7s cubic-bezier(.2,.8,.2,1)}
p{margin:10px 0 0;color:#cfc3c1;opacity:0;transform:translateY(24px);transition:opacity .7s .15s,transform .7s .15s}
section.active h2,section.active p{opacity:1;transform:none}
const io = new IntersectionObserver(entries => {
entries.forEach(e => e.target.classList.toggle('active', e.isIntersecting));
}, { threshold: 0.6 });
document.querySelectorAll('section').forEach(s => io.observe(s));
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Scroll snap sections</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}
html{scroll-snap-type:y mandatory}
body{display:block;place-items:normal;height:auto}
section{height:100vh;scroll-snap-align:start;display:grid;place-items:center;text-align:center;padding:20px}
section:nth-child(1){background:#140807}section:nth-child(2){background:#2a0f0b}
section:nth-child(3){background:#1f1210}section:nth-child(4){background:#2f1512}
h2{margin:0;font-size:clamp(2.2rem,9vw,4rem);letter-spacing:-.03em;opacity:0;transform:translateY(40px) scale(.96);
transition:opacity .7s cubic-bezier(.2,.8,.2,1),transform .7s cubic-bezier(.2,.8,.2,1)}
p{margin:10px 0 0;color:#cfc3c1;opacity:0;transform:translateY(24px);transition:opacity .7s .15s,transform .7s .15s}
section.active h2,section.active p{opacity:1;transform:none}
</style></head>
<body>
<section><div><h2>One</h2><p>Scroll to snap to the next screen</p></div></section>
<section><div><h2>Two</h2><p>Text animates in on arrival</p></div></section>
<section><div><h2>Three</h2><p>Each section is a full screen</p></div></section>
<section><div><h2>Four</h2><p>The end</p></div></section>
<script>
const io = new IntersectionObserver(entries => {
entries.forEach(e => e.target.classList.toggle('active', e.isIntersecting));
}, { threshold: 0.6 });
document.querySelectorAll('section').forEach(s => io.observe(s));
</script>
</body></html>
Good for
Presentation-style pages, onboarding flows and product stories.
Watch out for
Use scroll-snap-type: y proximity on long content so people are never trapped. Mandatory suits only short, screen-sized sections.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.