Clip-path curtain
Artwork panels open from the centre like a curtain, tied directly to scroll position.
Tiles rise into place in a short wave as each row enters the viewport.
An IntersectionObserver adds an "in" class when a tile is 20% visible. Each tile gets a --i index from its column, and transition-delay multiplies it, so a row arrives left to right instead of all at once.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Staggered fade-up</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: #f3efe7; color: #2a2238; }
.intro { height: 70vh; display: grid; place-content: center; text-align: center; }
.intro h1 { font-size: 3rem; margin: 0 0 .25em; }
.grid {
display: grid; gap: 16px; padding: 24px;
grid-template-columns: repeat(4, 1fr);
max-width: 900px; margin: 0 auto 50vh;
}
.tile {
aspect-ratio: 1; border-radius: 14px; background: var(--c);
opacity: 0; transform: translateY(48px) scale(.96);
transition: opacity .6s ease, transform .7s cubic-bezier(.2,.7,.2,1);
transition-delay: calc(var(--i) * 80ms);
}
.tile.in { opacity: 1; transform: none; }
@media (max-width: 500px) { .grid { grid-template-columns: repeat(2, 1fr); } }
</style>
</head>
<body>
<section class="intro">
<h1>Scroll down</h1>
<p>Tiles fade up in a staggered wave.</p>
</section>
<div class="grid" id="grid"></div>
<script>
const grid = document.getElementById('grid');
const colors = ['#ff7a59', '#ffc145', '#5b8cff', '#2ec4b6', '#b388eb', '#ef476f'];
for (let i = 0; i < 24; i++) {
const t = document.createElement('div');
t.className = 'tile';
t.style.setProperty('--c', colors[(i * 7) % colors.length]);
t.style.setProperty('--i', i % 4);
grid.appendChild(t);
}
const io = new IntersectionObserver(entries => {
entries.forEach(e => e.target.classList.toggle('in', e.isIntersecting));
}, { threshold: 0.2 });
document.querySelectorAll('.tile').forEach(t => io.observe(t));
</script>
</body>
</html>