Spotlight circle

A circle of light grows from the centre of a black screen until the whole scene is lit.

How it works

The scene sits in a pinned stage with clip-path: circle(). Scroll progress sets the radius from 0 to 75%, which is enough to cover the corners of any rectangle. The heading on the black layer fades as the circle grows past it.

Full file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Spotlight circle</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: #000; color: #fff; }
.track { height: 250vh; }
.stage { position: sticky; top: 0; height: 100vh; overflow: hidden; display: grid; place-items: center; }
.stage h2 { font: 900 clamp(2.6rem, 10vw, 7rem)/1 system-ui, sans-serif; letter-spacing: -.04em; margin: 0; }
.dark { position: absolute; color: #333; }
.scene { position: absolute; inset: 0; display: grid; place-items: center; clip-path: circle(0% at 50% 50%); color: #1b1b1b;
  background: radial-gradient(circle at 50% 50%, #fff6d5, #ffd166 40%, #ef476f 80%); }
.after { min-height: 70vh; display: grid; place-content: center; padding: 0 10vw; background: #ef476f; }
.after p { font-size: 1.6rem; max-width: 28ch; text-align: center; }
</style>
</head>
<body>
<section class="track">
  <div class="stage">
    <h2 class="dark" id="dark">Lights on</h2>
    <div class="scene" id="scene"><h2>Hello, stage.</h2></div>
  </div>
</section>
<section class="after"><p>Clip-path circles are cheap to animate and work everywhere.</p></section>
<script>
const scene = document.getElementById('scene'), dark = document.getElementById('dark');
const track = document.querySelector('.track');
function update() {
  const r = track.getBoundingClientRect();
  const p = Math.min(1, Math.max(0, -r.top / (r.height - innerHeight)));
  scene.style.clipPath = 'circle(' + (p * 75).toFixed(2) + '% at 50% 50%)';
  dark.style.opacity = (1 - p * 2.5).toFixed(2);
}
addEventListener('scroll', update, { passive: true });
update();
</script>
</body>
</html>

More reveal effects