Three.js Scroll Portal Gate Sequence — GSAP Ring Flythrough

Three.js Scroll Portal Gate Sequence · Scroll · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Ten discrete TorusGeometry gates threaded along Z, each independently colored, tilted, and reacted to
Per-frame Math.abs(camera.z - gate.z) proximity check replaces ten separate ScrollTrigger callbacks
Two-signal glow: proximity ramp plus an offset sine pulse so distant gates stay visually alive
Shader-free reaction: color offsetHSL, opacity, and mesh scale combine into a convincing pulse-and-brighten
Faint inner CircleGeometry membrane disc per gate gives a surface to visually pop through, not just an outline
Recycled 500-point particle field wraps by totalDepth instead of respawning, for zero-allocation motion
FogExp2 matched to scene.background hides the fixed gate count at the far end of the sequence
Live HUD counter derived from counting passed gates, reversible in both directions with scroll

About this UI Snippet

How to Build a Scroll-Driven Portal Gate Sequence With Three.js and GSAP

Screenshot of the Three.js Scroll Portal Gate Sequence snippet rendered live

The Three.js Scroll Portal Gate Sequence snippet flies the camera through ten discrete glowing rings spaced along the Z axis, each one pulsing brighter right as the camera crosses its plane, using GSAP's ScrollTrigger to scrub a single camera-position value and a per-frame distance check to drive each ring's reaction. Where the scroll tunnel snippet builds one continuous TubeGeometry corridor, this snippet is built from ten independent TorusGeometry gates the camera threads one at a time, which changes both the geometry approach and how the "reactive" moment is detected.

Discrete gates instead of a continuous tube

Each gate is its own THREE.Mesh combining a glowing TorusGeometry ring with a faint inner CircleGeometry "membrane" disc, positioned at a fixed Z offset with a small random-looking tilt and rotation so the sequence reads as hand-placed rather than mechanically repeated. Because gates are discrete objects rather than samples along a shared curve, each one can be independently colored, scaled, and reacted to — a flexibility a single extruded tube does not offer, at the cost of needing per-gate state tracked in a plain array instead of one shared geometry.

Comparing camera Z to ring Z, not ScrollTrigger callbacks per ring

A naive implementation might register ten separate ScrollTrigger instances, one per gate, each firing an enter/leave callback. This snippet instead keeps a single ScrollTrigger that scrubs one travel.z value for the whole flight, and every animation frame computes Math.abs(camera.position.z - gate.z) for all ten gates to derive a 0–1 proximity value per ring. This is simpler to reason about, avoids ten separate trigger/scrub configurations drifting out of sync with each other, and means adding an eleventh gate requires no new ScrollTrigger wiring at all — just another entry in the gates array.

Two-part glow: proximity plus a sine pulse

Each gate's brightness boost multiplies two signals: proximity, which ramps up as the camera nears the ring's plane and back down after it passes, and a continuous per-gate sine pulse offset by its index so the ten rings do not all throb in unison. The proximity term gives the "the camera is passing through me right now" reaction the spec calls for, while the sine term keeps distant gates visually alive rather than static, so the corridor never looks like a row of inert shapes waiting to be triggered.

Color and scale react without a shader

Rather than writing a custom fragment shader for the glow, the ring's MeshBasicMaterial.color is nudged toward white with Color.offsetHSL, its opacity is raised, and the mesh itself is scaled up slightly — three cheap, GPU-shader-free properties that combine into a convincing pulse-and-brighten effect. This mirrors the philosophy in synthwave terrain and the scroll tunnel: favor material and transform tweaks over custom GLSL wherever the visual target allows it, since it keeps the snippet copy-pasteable with zero shader compilation risk.

A recycled particle field for depth cueing

Five hundred points drift toward the camera and wrap around using modular Z arithmetic once they pass it, exactly like the corridor-filling technique used in the starfield warp snippet, so the space between gates never reads as empty black void. Combined with FogExp2 matched to the background color, distant gates fade in gradually rather than popping into existence, which hides the fixed GATE_COUNT boundary at the far end of the sequence.

One scrubbed Z value, independent micro-drift

The camera's Z position is fully driven by the GSAP scrub, but X and Y get a small independent sine/cosine drift based on elapsed clock time rather than scroll, so the flight never feels perfectly rigid even though forward progress is 100% scroll-controlled. Because only Z is tied to scroll, scrolling back up reverses gate order and undoes every ring's "passed" flag exactly, with the drift simply continuing to animate in the background as a live-clock-driven flourish, similar in spirit to the scroll camera path snippet's handling of secondary motion.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not need to work out from scratch how ten independent rings can each react to a single scrubbed camera position. Paste this snippet's HTML, CSS, and JS into an AI assistant like Claude and ask it to explain why proximity is computed per-gate every frame instead of registering a ScrollTrigger callback per ring, or why the glow combines a proximity ramp with an offset sine pulse. The same assistant can help you extend the effect, for instance making each gate's color shift as the camera passes through it (not just brighten), adding a screen-space flash on the exact frame a gate is crossed, or generating gate colors from a brand palette instead of an HSL hue sweep. It can also help with performance, such as converting the ring and disc meshes into two InstancedMesh calls if you scale the gate count up significantly. Treat the code as a starting point to question and reshape, not a finished, untouchable 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 portal gate sequence" in plain HTML, CSS, and JavaScript using Three.js 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 it and updated on window resize including aspect ratio.
- Create 8-12 THREE.TorusGeometry "gates" positioned at even intervals along the negative Z axis, each with a distinct HSL hue, a slight per-gate rotation/tilt offset, and a faint inner CircleGeometry "membrane" disc behind each ring using a transparent MeshBasicMaterial.
- Add a drifting particle field (THREE.Points, several hundred points) scattered through the corridor depth that wraps its Z position using modular arithmetic once particles pass the camera, instead of being respawned as new objects.
- Add THREE.FogExp2 whose color matches the page/scene background so distant gates and particles fade out rather than popping in or hitting a visible edge.
- Register a single GSAP tween on a ScrollTrigger targeting the pinned section, with pin: true, start at top top, a numeric scrub around 0.6, and an end sized to the total corridor depth, animating one plain camera-position-driving value (e.g. travel.z) from just outside the first gate to just past the last gate.
- Every animation frame (requestAnimationFrame, independent of the scroll callback): set camera.position.z from the scrubbed value, add a small independent sine/cosine drift on X and Y driven by elapsed clock time (not scroll), and call camera.lookAt a point ahead of the camera.
- Each frame, for every gate compute the absolute distance between camera Z and that gate's Z, derive a 0-1 proximity value from it, combine it with a per-gate sine pulse (offset by index) into a single boost value, and use that boost to brighten the ring's color (via HSL lightness offset), raise its opacity, and scale the mesh up slightly — with no custom shader.
- Track a "passed" boolean per gate based on whether the camera has crossed its Z plane, and display a live "gate n of total" counter derived from counting passed gates.
- Confirm scrolling back up reverses the entire sequence, unpassing gates and pulling the camera backward through each gate exactly, since the position is fully scrubbed rather than a one-way timer.

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.

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, before the snippet JS.
  2. 2
    Paste HTML, CSS, and JSA pinned corridor of ten hued portal gates appears with a live "GATE n / 10" HUD counter.
  3. 3
    Scroll downThe camera flies forward through each ring in sequence; every gate brightens, scales up, and glows as the camera crosses its plane.
  4. 4
    Scroll back upThe flight reverses exactly and each gate's passed state resets, since travel.z is fully scrubbed rather than one-shot.
  5. 5
    Add or remove gatesChange GATE_COUNT and GATE_SPACING; the ScrollTrigger end and particle wrap distance both derive from totalDepth automatically.
  6. 6
    Retune the pulse and pass-through reactionAdjust the proximity falloff distance (the /14 divisor) or the sine pulse speed and per-ring color boost to change how dramatic each gate reacts.

Real-world uses

Common Use Cases

Product launch and chapter-based landing pages
Assign one gate per product feature or pricing tier so each scroll-triggered pulse lines up with a new content block.
Game and metaverse portal promos
A literal gate-threading sequence matches "portal," "dimension," or "level select" framing for game and Web3 marketing sites.
Event countdown and reveal pages
Color each gate to represent a countdown stage, brightening in sequence as visitors scroll toward a launch date reveal.
Music visualizer and album pages
Sync gate hues to a tracklist and let each pulse coincide with a track change as the page scrolls, similar to a scroll tunnel music intro.
Teaching per-object scroll reactions
A compact example of driving many independent Three.js objects from one scrubbed value instead of one ScrollTrigger per object.
Portfolio section dividers
Use each gate as a transition between portfolio categories, distinct from the corridor style of a synthwave terrain drive-through.

Got questions?

Frequently Asked Questions

Ten separate ScrollTrigger instances would mean ten independent scrub configurations that could drift out of sync, and would need re-registering whenever gate count or spacing changes. A single ScrollTrigger scrubs one travel.z value for the whole flight, and a cheap per-frame Math.abs(camera.position.z - gate.z) check across a plain array gives every gate its own reaction with no extra scroll wiring, so adding an eleventh gate is just one more array entry.

Proximity alone would make distant gates completely static, which reads as inert rather than alive. Multiplying it by a continuous per-gate sine pulse, offset by index so gates do not throb in unison, keeps the whole corridor visually active while still giving a clear, distinct brightening spike exactly as the camera crosses each ring's plane.

MeshBasicMaterial.color.offsetHSL, opacity, and mesh scale are three GPU-cheap, shader-free properties that together produce a convincing pulse-and-brighten effect without writing or compiling GLSL. This keeps the snippet fully copy-pasteable with no shader compilation risk across browsers, the same philosophy used for the ring rendering in the scroll tunnel snippet.

The particle field is a single THREE.Points draw call with one Float32Array updated in place each frame rather than five hundred individual objects, and the ten gates are twenty total meshes (ring plus disc), so draw calls stay low. FogExp2 additionally lets distant gates and particles fade out visually without needing to cull them from the scene graph.

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 gates array, particle field, and GSAP timeline inside a mount effect against a canvas ref, and on cleanup kill the ScrollTrigger instance (or revert a gsap.context), dispose of each ring and disc geometry/material, and call renderer.dispose() so WebGL resources and the scroll pin are released on unmount.