Three.js Scroll Kaleidoscope Mandala — Radial Instanced Bloom

Three.js Scroll Kaleidoscope Mandala · Scroll · Plain HTML, CSS & JS · Live preview

What's included

Features

Three THREE.InstancedMesh color tiers render hundreds of octahedra (SEGMENTS × RINGS × PER_RING) in three draw calls
Per-instance polar coordinates (angle, radius) precomputed once from radial symmetry constants, not per frame
Ring groups are split across solid-color instanced tiers (magenta core, cyan mid, gold outer) for a designed palette with zero per-frame shading cost
Bloom radius and scale are both pure functions of one smoothstepped progress value for guaranteed reversibility
Ring-dependent rotational drift makes outer rings spin faster than inner ones for a living kaleidoscope feel
Individual per-instance spin speed and phase randomized once for organic, non-uniform shape rotation
Camera dollies inward as the mandala blooms to frame the finished symmetric pattern tightly
Fully reversible pinned scroll animation — scrolling up collapses the mandala back to its origin point

About this UI Snippet

How to Build a Scroll-Driven Kaleidoscope Mandala With Three.js

Screenshot of the Three.js Scroll Kaleidoscope Mandala snippet rendered live

The Three.js Scroll Kaleidoscope Mandala snippet arranges hundreds of small octahedra into repeating radial segments and rings using a handful of THREE.InstancedMesh color tiers. As the visitor scrolls through a pinned stage, every shape blooms outward from a collapsed center point to its assigned polar position, rotating and growing along the way, producing a symmetric mandala pattern built entirely from precomputed polar coordinates.

Radial segments times rings times per-ring shapes

The instance count is the product of three constants: SEGMENTS (the mandala's rotational symmetry, like the mirrors in a kaleidoscope), RINGS (concentric bands from center to edge), and PER_RING (shapes clustered within each segment/ring cell). Looping over all three nested indices once at startup assigns every instance a fixed target angle and target radius — the angle comes from (segment / SEGMENTS) * TAU plus a small per-shape offset, and the radius comes from the ring's normalized position scaled up to the mandala's outer edge. This is the same "precompute polar targets, then animate toward them" approach used for spiral placement in galaxy formation, applied here with rotational symmetry instead of arm-based placement.

Three color-tier InstancedMesh objects instead of per-instance vertex color

All octahedra share one THREE.OctahedronGeometry, but rather than relying on per-instance instanced vertex colors (a feature added to three.js after r128, the version this snippet targets), rings are grouped into three color tiers — inner, mid, and outer — each rendered by its own THREE.InstancedMesh with a distinct solid-color MeshStandardMaterial. That keeps the whole mandala at exactly three draw calls, portable to any three.js build, while still reading as a magenta-to-cyan-to-gold gradient from core to rim.

Bloom driven by one scrubbed progress value

The scrubbed form.t is smoothstepped into eased, which scales each instance's live radius (targetRadius * eased) and scale (0.15 + eased * 0.85), so at eased = 0 every shape sits collapsed and tiny at the origin, and at eased = 1 the full mandala is bloomed to its precomputed radius. Because radius and scale are both pure functions of the same eased value, scrolling back up collapses the mandala smoothly back to a point with no extra bookkeeping.

Layered rotation for a living kaleidoscope feel

On top of the bloom, each instance also spins on its own axis at an individually randomized speed (spinSpeed), the whole mesh slowly rotates as a group, and each instance's angular position additionally drifts with a ring-dependent multiple of that global spin — outer rings rotate faster than inner ones, echoing how real kaleidoscopes show independent motion per mirrored layer. This constant low-amplitude motion runs from an accumulating clock value, but because it never touches eased (only supplies orbital variation), it does not break the reversibility of the bloom itself.

Camera pull-in reinforces the bloom

The camera dollies from z = 26 down toward z = 18 as the mandala blooms, framing the finished pattern tighter than the collapsed starting point — a similar reveal-camera technique to the pull-back used in origami crane unfold, just inverted in direction to suit a symmetric pattern instead of a folding form.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not need to reverse-engineer how a radially symmetric mandala blooms from a single point using one draw call. Paste this snippet's HTML, CSS, and JS into an AI assistant like Claude and ask it to explain how the nested SEGMENTS/RINGS/PER_RING loop produces polar coordinates, or why ring-dependent rotation speed makes the pattern feel alive rather than static. The same assistant can help you extend it — ask it to add a second InstancedMesh layer with a different geometry for more visual variety, tie the color gradient to scroll velocity, or add mirrored reflection across a second axis for a more intricate kaleidoscope. 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 kaleidoscope mandala bloom" 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.
- Render several hundred small polyhedra (e.g. octahedra) using two or three THREE.InstancedMesh objects, one per color tier, so the whole mandala renders in a small fixed number of draw calls regardless of instance count.
- Generate instance targets with a triple nested loop over a number of radial segments (rotational symmetry), a number of concentric rings, and a number of shapes per ring cell, computing each instance's target angle as (segment / totalSegments) * TAU plus a small offset, and its target radius from the ring's normalized position scaled to an outer radius.
- Assign each ring to one of a small number of color tiers (inner, mid, outer), each rendered by its own InstancedMesh with a distinct solid material color, so the mandala reads as a gradient without per-instance vertex colors.
- 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.
- Every animation frame, apply smoothstep easing to the scrubbed progress, then compute each instance's live radius and scale as that value times its precomputed target, convert its polar coordinates to Cartesian, and write the resulting transform via a reused dummy Object3D and setMatrixAt.
- Add continuous per-instance rotation using an accumulating clock and a randomized per-instance spin speed, plus a slow global rotation of the whole mandala with an outer-rings-spin-faster relationship to ring index, without letting any of that continuous motion affect the bloom progress itself.
- Dolly the camera closer as the mandala blooms.
- Confirm scrolling back up reverses the entire bloom smoothly, collapsing every shape back toward the origin, since radius and scale are pure functions of the one progress value.

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="kdm-stage" id="kdmStage">
  <div class="kdm-intro-overlay"><p>Scroll ↓ to bloom the mandala into full symmetry</p></div>
  <canvas id="kdmCanvas"></canvas>
  <div class="kdm-hud"><span id="kdmPct">0</span>% bloomed</div>
</section>
<section class="kdm-bottom"><p>A fully bloomed, radially symmetric mandala.</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 JSA collapsed point of shapes appears inside a pinned 3D stage with a live "% bloomed" read-out.
  3. 3
    Scroll downHundreds of instanced octahedra bloom outward into radial rings, forming a symmetric mandala while spinning.
  4. 4
    Scroll back upThe mandala collapses back to a point exactly in reverse, since radius and scale are pure functions of one progress value.
  5. 5
    Retune the shapeChange SEGMENTS for more or fewer kaleidoscope mirrors, or RINGS/PER_RING for a denser or sparser pattern.
  6. 6
    Adjust the pacingChange the ScrollTrigger end value (+=450%) for a slower or snappier bloom relative to scroll distance.

Real-world uses

Common Use Cases

Generative and geometric art portfolios
Showcase symmetry-driven generative work with a piece that visibly assembles its own radial structure.
Meditation and wellness app landing pages
A blooming mandala suits mindfulness, yoga, or spiritual-wellness hero sections better than a static image.
Event and festival microsites
Open a design festival or arts-and-culture event page with a radially symmetric centerpiece animation.
Brand reveal intros
Pair with a logo mark at the mandala center for a distinctive scroll-triggered brand reveal moment.
Teaching InstancedMesh and polar math
A compact example of converting nested loop indices into polar coordinates for radial symmetry.
Puzzle or rhythm game menus
Use the bloom as a scroll-triggered menu backdrop, similar in spirit to wireframe globe spin as an ambient title-screen element.

Got questions?

Frequently Asked Questions

A mandala built from hundreds of individual THREE.Mesh objects would issue one draw call per shape, which quickly becomes a bottleneck. THREE.InstancedMesh solves that, but this snippet targets three.js r128, and splits the rings into three solid-color materials (one InstancedMesh per color tier) rather than relying on per-instance instanced vertex colors, so it renders in exactly three draw calls on any three.js version without depending on newer instance-color APIs.

A triple nested loop over SEGMENTS, RINGS, and PER_RING assigns each instance index a target angle (segment fraction of a full turn, plus a small per-shape offset) and a target radius (ring fraction scaled to the outer edge). These polar targets are stored once in Float32Arrays and never recomputed, so the render loop only needs to scale them by the live progress value.

The radius and scale of every instance are pure functions of the smoothstepped progress value, so reversing scroll direction immediately reverses the bloom. The continuous per-instance spin and ring-dependent rotational drift use an independent accumulating clock purely for orbital variation — they never gate or accumulate into the bloom progress itself, so they add life to the animation without breaking its reversibility.

Yes — SEGMENTS controls how many rotational mirrors the mandala has. Because all target angles and radii are recomputed from SEGMENTS, RINGS, and PER_RING at load time, changing SEGMENTS to a different integer (say 6 for a hexagonal mandala or 14 for a finer one) immediately changes the pattern's symmetry order without touching the animation logic.

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 InstancedMesh, target arrays, and GSAP ScrollTrigger inside a mount effect keyed to a canvas ref, and on unmount kill the ScrollTrigger instance, dispose geometry/material, and call renderer.dispose().