Line mask reveal

Headlines slide up from behind an invisible mask, one line after another.

How it works

Each line is wrapped in a span with overflow: hidden; the inner span starts at translateY(105%). When the heading enters the viewport, a class moves the inner spans to zero, with a delay per line so the heading builds from the top.

Full file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Line mask reveal</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; }
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; -webkit-font-smoothing: antialiased; }

body { background: #fffaf0; color: #1b1b1b; }
.block { min-height: 100vh; display: grid; align-content: center; padding: 0 8vw; }
.block.alt { background: #1b1b1b; color: #fffaf0; }
.split { font: 800 clamp(2.4rem, 8vw, 5.5rem)/1 system-ui, sans-serif; letter-spacing: -.03em; margin: 0; }
.split > span { display: block; overflow: hidden; padding-bottom: .06em; }
.split > span > span {
  display: inline-block; transform: translateY(105%);
  transition: transform .8s cubic-bezier(.2,.8,.2,1);
}
.split > span:nth-child(2) > span { transition-delay: .1s; }
.split > span:nth-child(3) > span { transition-delay: .2s; }
.split.in > span > span { transform: none; }
</style>
</head>
<body>
<section class="block"><h2 class="split"><span><span>Big ideas</span></span><span><span>arrive one</span></span><span><span>line at a time.</span></span></h2></section>
<section class="block alt"><h2 class="split"><span><span>Masks hide</span></span><span><span>the motion,</span></span><span><span>not the words.</span></span></h2></section>
<section class="block"><h2 class="split"><span><span>Scroll back up</span></span><span><span>and they leave</span></span><span><span>the same way.</span></span></h2></section>
<script>
const io = new IntersectionObserver(entries => {
  entries.forEach(e => e.target.classList.toggle('in', e.isIntersecting));
}, { threshold: 0.5 });
document.querySelectorAll('.split').forEach(h => io.observe(h));
</script>
</body>
</html>

More reveal effects