Reveal on scroll
Elements fade up the first time they enter the viewport, detected with IntersectionObserver.
CSSJavaScriptBeginner
<p class="hint">Scroll down inside this frame</p>
<div class="col">
<div class="block reveal"><h2>One</h2><p>I appeared as you scrolled.</p></div>
<div class="block reveal"><h2>Two</h2><p>Each block reveals once.</p></div>
<div class="block reveal"><h2>Three</h2><p>Nothing is lost if JS is off.</p></div>
<div class="block reveal"><h2>Four</h2><p>Unobserved after the first show.</p></div>
<div class="block reveal"><h2>Five</h2><p>Done.</p></div>
</div>
body{display:block;height:auto;padding:40px 0 80px}
.col{width:min(420px,88vw);margin:0 auto;display:grid;gap:28px}
.hint{text-align:center;color:#a6938f;font-size:14px;margin-bottom:12px}
.block{padding:28px;border-radius:14px;background:#2a1513;border:1px solid #4a2e2a;min-height:140px}
.block h2{margin:0 0 6px}.block p{margin:0;color:#cfc3c1}
/* only hide when JS is running */
.js .reveal{opacity:0;transform:translateY(36px);transition:opacity .8s cubic-bezier(.2,.8,.2,1),transform .8s cubic-bezier(.2,.8,.2,1)}
.js .reveal.in{opacity:1;transform:none}
@media (prefers-reduced-motion:reduce){.js .reveal{opacity:1;transform:none;transition:none}}
document.documentElement.classList.add('js');
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (!e.isIntersecting) return;
e.target.classList.add('in');
io.unobserve(e.target);
});
}, { threshold: 0.2 });
document.querySelectorAll('.reveal').forEach(el => io.observe(el));
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Reveal 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;padding:40px 0 80px}
.col{width:min(420px,88vw);margin:0 auto;display:grid;gap:28px}
.hint{text-align:center;color:#a6938f;font-size:14px;margin-bottom:12px}
.block{padding:28px;border-radius:14px;background:#2a1513;border:1px solid #4a2e2a;min-height:140px}
.block h2{margin:0 0 6px}.block p{margin:0;color:#cfc3c1}
/* only hide when JS is running */
.js .reveal{opacity:0;transform:translateY(36px);transition:opacity .8s cubic-bezier(.2,.8,.2,1),transform .8s cubic-bezier(.2,.8,.2,1)}
.js .reveal.in{opacity:1;transform:none}
@media (prefers-reduced-motion:reduce){.js .reveal{opacity:1;transform:none;transition:none}}
</style></head>
<body>
<p class="hint">Scroll down inside this frame</p>
<div class="col">
<div class="block reveal"><h2>One</h2><p>I appeared as you scrolled.</p></div>
<div class="block reveal"><h2>Two</h2><p>Each block reveals once.</p></div>
<div class="block reveal"><h2>Three</h2><p>Nothing is lost if JS is off.</p></div>
<div class="block reveal"><h2>Four</h2><p>Unobserved after the first show.</p></div>
<div class="block reveal"><h2>Five</h2><p>Done.</p></div>
</div>
<script>
document.documentElement.classList.add('js');
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (!e.isIntersecting) return;
e.target.classList.add('in');
io.unobserve(e.target);
});
}, { threshold: 0.2 });
document.querySelectorAll('.reveal').forEach(el => io.observe(el));
</script>
</body></html>
Good for
Almost every landing page section. The most common scroll animation there is.
Watch out for
Hide with JS only (add a class on html), so the content is still visible if scripts fail. Unobserve after revealing to save work.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.