Three.js Scroll Solar Eclipse Transit — Corona Reveal Animation

Three.js Scroll Solar Eclipse Transit · Scroll · Plain HTML, CSS & JS · Live preview

What's included

Features

Fixed self-lit sun sphere paired with a lerping moon sphere driven by Vector3.lerpVectors between two fixed points
Eclipse alignment is computed each frame from live 2D screen-plane distance, not directly from scroll progress
Additive-blended RingGeometry corona billboards toward the camera every frame via lookAt
Corona opacity and scale driven by squared alignment for a slow build followed by a rapid bloom near full eclipse
Sun light intensity and color shift subtly as alignment increases, reinforcing the sense of the light being obscured
Static single-draw-call starfield background establishes a deep-space setting with zero per-frame cost
Zero per-frame allocations: reused Vector3 lerp targets and direct property writes throughout the render loop
Fully reversible pinned scroll animation — scrolling up separates the bodies and fades the corona in reverse

About this UI Snippet

How to Build a Scroll-Driven Solar Eclipse Transit With Three.js

Screenshot of the Three.js Scroll Solar Eclipse Transit snippet rendered live

The Three.js Scroll Solar Eclipse Transit snippet slides a dark moon sphere across the scene toward a fixed, glowing sun as the visitor scrolls through a pinned stage, growing a ring-shaped corona effect the closer the two bodies get to perfect alignment — a small, focused physical simulation built from two spheres, one ring mesh, and a distance calculation.

A fixed sun and a lerping moon

The sun sits permanently at the origin as a MeshBasicMaterial sphere (self-lit, so it always reads as a light source regardless of scene lighting) paired with a THREE.PointLight for glow on nearby geometry. The moon, by contrast, starts far to one side (moonStart) and every frame its position is set via Vector3.lerpVectors(moonStart, moonEnd, eased) — a direct interpolation between two fixed points using the smoothstepped scroll progress, the same fixed-endpoint lerp technique used for particle placement in galaxy formation, here applied to a single rigid body instead of thousands of particles.

Alignment computed from live 2D screen-plane distance, not just progress

Rather than tying the corona and lighting effects directly to the scrubbed eased value, this snippet computes a separate alignment value each frame from the actual live distance between the moon's and sun's x/y positions (ignoring z, since the eclipse effect is about screen-plane overlap from the camera's point of view). This means the corona and dimming effects are driven by *where the moon visually is*, not merely how far scroll has progressed — a subtle but important distinction that would matter if moonEnd were ever changed to not perfectly overlap the sun, or if the lerp path curved.

Additive-blended ring geometry for the corona

The corona is a flat THREE.RingGeometry with an inner radius just past the sun's radius and an outer radius further out, given a MeshBasicMaterial with blending: THREE.AdditiveBlending, depthWrite: false, and transparent: true. Its opacity is driven by alignmentSmooth (alignment squared, so the corona stays essentially invisible until the moon is quite close and then blooms in quickly near full eclipse), and it calls lookAt(camera.position) every frame so the flat ring always billboards to face the viewer regardless of camera position — a simple but effective substitute for a full glow sprite or bloom post-process pass.

Sun dimming and color shift reinforce the eclipse

As alignment increases, the sun's own PointLight intensity is reduced (simulating the moon blocking direct light) and the sun material's color is nudged toward a slightly warmer, whiter tone at full alignment — small touches that sell the sense of the light source being obscured even though the moon sphere and sun sphere are never boolean-clipped against each other.

Fully reversible via live distance, not a one-way flag

Because alignment is recomputed fresh from live positions every single frame rather than being set once and left, there is no "eclipse happened" flag to reset. Scrolling back up moves the moon back toward moonStart, live distance increases again, and the corona and dimming fade out exactly following the same alignment curve in reverse.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not need to reverse-engineer how a moon slides into alignment with a sun to reveal a glowing corona. Paste this snippet's HTML, CSS, and JS into an AI assistant like Claude and ask it to explain why alignment is computed from live distance rather than scroll progress directly, or how the additive-blended ring corona billboards toward the camera. The same assistant can help you extend it — ask it to add a subtle lens-flare sprite at full eclipse, animate visible solar prominences around the corona's edge, or add a small orbiting Earth silhouette in the foreground for scale. Treat the code as a conversation starter, not a finished artifact.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "scroll-scrubbed solar eclipse transit" in plain HTML, CSS, and JavaScript using Three.js, GSAP, and GSAP's ScrollTrigger plugin, all loaded from a CDN (no bundler, no build step).

Requirements:
- A pinned section containing a full-size canvas, with a WebGLRenderer and PerspectiveCamera sized to the canvas element (not window.innerWidth/innerHeight) and updated on window resize including aspect ratio.
- Create a fixed, self-lit sun sphere at the origin using MeshBasicMaterial (so it always reads as glowing regardless of scene lighting), paired with a THREE.PointLight for ambient glow.
- Create a dark moon sphere slightly larger in apparent radius than the sun, starting at a position offset far to one side, and every frame set its live position via Vector3.lerpVectors between that starting position and a target position centered on the sun, driven by a single scrubbed 0-1 progress value with smoothstep easing applied.
- Every frame, compute an "alignment" value from the live 2D (x, y) distance between the moon's and sun's current positions, not directly from the scroll progress value, normalized so it reaches 1 when the moon is centered on the sun and 0 when far away.
- Add a flat THREE.RingGeometry mesh as a corona, with an inner radius matching the sun's edge and a larger outer radius, using a MeshBasicMaterial with additive blending, depthWrite false, and transparent true; set its opacity each frame to the squared alignment value so it stays invisible until the moon is nearly overlapping the sun, and call lookAt(camera.position) on it every frame so it always faces the viewer.
- Slightly dim the sun's point light intensity and shift its material color as alignment increases, to reinforce the sense of the light source being obscured.
- Register a GSAP tween on a ScrollTrigger targeting the pinned section, with pin: true, start at top top, a numeric scrub, and a multi-hundred-percent end, animating a single plain progress value from 0 to 1 with linear easing, which drives the moon's lerp.
- Add a simple static starfield background (a single Points object with a few hundred randomly placed points) for a deep-space setting.
- Confirm scrolling back up reverses the transit smoothly: the moon slides back away from the sun and the corona fades out, since alignment is recomputed fresh every frame from live positions with no one-way flag.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<section class="sec-stage" id="secStage">
  <div class="sec-intro-overlay"><p>Scroll ↓ to bring the moon into alignment with the sun</p></div>
  <canvas id="secCanvas"></canvas>
  <div class="sec-hud"><span id="secPct">0</span>% eclipsed</div>
</section>
<section class="sec-bottom"><p>Total eclipse — the corona blazes around a darkened sun.</p></section>

Step by step

How to Use

  1. 1
    Load all three CDN scriptsAdd three.min.js, gsap.min.js, and ScrollTrigger.min.js from the CDN panel, in that order.
  2. 2
    Paste HTML, CSS, and JSThe sun sits alone with the moon offset far to one side inside a pinned 3D stage with a live "% eclipsed" read-out.
  3. 3
    Scroll downThe moon slides into alignment with the sun; as they overlap, a glowing corona ring blooms into view.
  4. 4
    Scroll back upThe moon slides back away and the corona fades, following the exact same alignment curve in reverse.
  5. 5
    Retune the transitChange moonStart/moonEnd for a different transit path, or the RingGeometry radii for a bigger or smaller corona.
  6. 6
    Adjust the pacingChange the ScrollTrigger end value (+=420%) for a slower or snappier alignment relative to scroll distance.

Real-world uses

Common Use Cases

Astronomy and space-education sites
Open a planetarium, observatory, or astronomy course page with an eclipse that assembles as visitors scroll.
Event and livestream landing pages
Promote an eclipse-viewing event or livestream with a scroll-driven preview of the moment of totality.
Editorial and long-form science articles
Pair with scroll text clip reveal captions explaining each phase of an eclipse.
Space and celestial-themed portfolios
Showcase lighting and additive-blending technique with a focused two-body orbital scene.
Teaching additive blending and billboarding
A compact real-world example of a camera-facing ring effect driven by live distance rather than a timeline.
Space or survival game loading screens
Pair with wireframe globe spin for a shared celestial visual language across menu screens.

Got questions?

Frequently Asked Questions

Tying corona opacity and sun dimming directly to the scrubbed progress value would assume the moon's path always ends in perfect visual overlap with the sun. By instead measuring the live x/y distance between the moon and sun every frame, the corona effect responds to where the moon actually appears relative to the sun on screen, which stays correct even if the transit path, camera angle, or endpoint were changed later.

A THREE.Sprite always faces the camera automatically but has less control over its exact inner/outer radius shape. A flat RingGeometry mesh gives precise control over the corona's inner and outer radii to match the sun's size, and calling lookAt(camera.position) every frame achieves the same camera-facing billboard behavior manually, which also makes it easy to combine with additive blending and a custom opacity curve.

Squaring the 0-1 alignment value before using it for opacity keeps the corona invisible for most of the moon's approach and then blooms it in rapidly only in the last stretch before full alignment, which matches how a real solar corona is imperceptible until totality is nearly reached, rather than fading in at a constant, unrealistic rate throughout the whole transit.

Yes. The moon's position is a pure lerp between two fixed points driven by scroll progress, and alignment, corona opacity, and sun dimming are all recomputed fresh from that live position every frame rather than being set once and left. Scrolling up moves the moon back toward its starting position and every dependent effect fades out following the exact same curve in reverse.

Yes. Click JSX for a React component, Vue for a Vue 3 SFC, Angular for a standalone component, or Tailwind for a React + Tailwind version. Build the sun, moon, corona mesh, and GSAP ScrollTrigger inside a mount effect keyed to a canvas ref, and on unmount kill the ScrollTrigger instance, dispose geometries/materials, and call renderer.dispose().