Back to Components
Interactive Bloom Particle Field – Three.js + Unreal Bloom
Component

Interactive Bloom Particle Field – Three.js + Unreal Bloom

CodewithLord
December 18, 2025

A high-performance interactive particle field built with Three.js featuring Unreal Bloom post-processing, mouse-driven forces, ripple interactions, layered particle systems, and cinematic camera motion.


🧠 Description


This project is an interactive bloom-powered particle field created using Three.js and Unreal Bloom post-processing.
Thousands of particles float in layered spherical formations and dynamically react to mouse movement and click-generated ripples, creating a living, breathing cosmic environment.


What Makes This Project Stand Out


  • 45,000+ GPU-accelerated particles
  • Unreal Bloom glow post-processing
  • Mouse-driven force fields
  • Click-based ripple shockwaves
  • Smooth particle spring physics
  • Color-shifting HSL gradients
  • Cinematic camera drift
  • Fully procedural (no models)

Core Concept


The scene is built using:

  • Layered particle spheres
  • Spring-damper physics to return particles to base positions
  • Mouse proximity forces
  • Expanding ripple waves
  • Additive blending + bloom for glow

All motion emerges from math and interaction — no pre-baked animations.




💻 Step 1: HTML Structure


Complete HTML Code


1<!DOCTYPE html> 2<html lang="en"> 3 4 <head> 5 <meta charset="UTF-8"> 6 <title>Interactive Bloom Particle Field</title> 7 8 9 </head> 10 11 <body> 12 <style> 13 * { 14 margin: 0; 15 padding: 0; 16 box-sizing: border-box; 17 } 18 html, body { 19 width: 100%; 20 height: 100%; 21 overflow: hidden; 22 } 23 #container { 24 position: fixed; 25 width: 100%; 26 height: 100%; 27 background: radial-gradient(circle at 50% 50%, 28 #1a0632 0%, 29 #140426 25%, 30 #0c021a 50%, 31 #06020e 75%, 32 #020108 100% 33 ); 34 } 35 canvas { 36 display: block; 37 width: 100%; 38 height: 100%; 39 } 40 .glow-overlay { 41 position: fixed; 42 top: 0; 43 left: 0; 44 width: 100%; 45 height: 100%; 46 pointer-events: none; 47 background: radial-gradient(circle at 50% 50%, 48 rgba(120, 50, 255, 0.05) 0%, 49 rgba(80, 40, 200, 0.03) 40%, 50 transparent 70%); 51 mix-blend-mode: screen; 52 } 53</style> 54 55<div id="container"></div> 56<div class="glow-overlay"></div> 57 58<script type="importmap"> 59{ 60 "imports": { 61 "three": "https://cdn.jsdelivr.net/npm/three@0.166.1/build/three.module.js", 62 "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.166.1/examples/jsm/" 63 } 64} 65</script> 66 67<script type="module"> 68 import * as THREE from 'three'; 69 import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; 70 import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; 71 import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; 72 73 let scene, camera, renderer, composer; 74 let particleLayers = []; 75 let time = 0; 76 const mouse = new THREE.Vector3(0, 0, 0); 77 const targetMouse = new THREE.Vector3(0, 0, 0); 78 const mouseRadius = 40; 79 let ripples = []; 80 81 const layersConfig = [ 82 { 83 count: 20000, 84 size: 0.3, 85 colorRange: { hue: [0.75, 0.9], sat: [0.7, 1], light: [0.5, 0.7] }, 86 rotationSpeed: 0.001 87 }, 88 { 89 count: 25000, 90 size: 0.2, 91 colorRange: { hue: [0.45, 0.6], sat: [0.6, 0.8], light: [0.4, 0.6] }, 92 rotationSpeed: 0.0005 93 } 94 ]; 95 96 function createParticleSystem(config) { 97 const geometry = new THREE.BufferGeometry(); 98 const positions = new Float32Array(config.count * 3); 99 const colors = new Float32Array(config.count * 3); 100 const basePositions = new Float32Array(config.count * 3); 101 const baseColors = new Float32Array(config.count * 3); 102 103 for (let i = 0; i < config.count; i++) { 104 const i3 = i * 3; 105 106 const radius = 25 + Math.random() * 30; 107 const theta = Math.random() * Math.PI * 2; 108 const phi = Math.acos(2 * Math.random() - 1); 109 110 const x = radius * Math.sin(phi) * Math.cos(theta); 111 const y = radius * Math.sin(phi) * Math.sin(theta); 112 const z = radius * Math.cos(phi); 113 114 positions[i3] = x; 115 positions[i3 + 1] = y; 116 positions[i3 + 2] = z; 117 118 basePositions[i3] = x; 119 basePositions[i3 + 1] = y; 120 basePositions[i3 + 2] = z; 121 122 const dist = Math.sqrt(x * x + y * y + z * z) / 55; 123 const hue = THREE.MathUtils.lerp(config.colorRange.hue[0], config.colorRange.hue[1], dist); 124 const sat = THREE.MathUtils.lerp(config.colorRange.sat[0], config.colorRange.sat[1], dist); 125 const light = THREE.MathUtils.lerp(config.colorRange.light[0], config.colorRange.light[1], dist); 126 127 const color = new THREE.Color().setHSL(hue, sat, light); 128 colors[i3] = color.r; 129 colors[i3 + 1] = color.g; 130 colors[i3 + 2] = color.b; 131 132 baseColors[i3] = color.r; 133 baseColors[i3 + 1] = color.g; 134 baseColors[i3 + 2] = color.b; 135 } 136 137 geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); 138 geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); 139 140 const textureLoader = new THREE.TextureLoader(); 141 const particleTexture = textureLoader.load('https://placehold.co/32x32/ffffff/ffffff.png?text=+'); 142 143 const material = new THREE.PointsMaterial({ 144 size: config.size, 145 vertexColors: true, 146 transparent: true, 147 opacity: 0.8, 148 blending: THREE.AdditiveBlending, 149 depthWrite: false, 150 sizeAttenuation: true, 151 map: particleTexture 152 }); 153 154 const points = new THREE.Points(geometry, material); 155 points.userData = { 156 velocities: new Float32Array(config.count * 3), 157 basePositions, 158 baseColors, 159 colorVelocities: new Float32Array(config.count * 3), 160 rotationSpeed: config.rotationSpeed 161 }; 162 163 return points; 164 } 165 166 function createRipple(x, y) { 167 ripples.push({ 168 x, y, 169 radius: 0, 170 strength: 2.5, 171 maxRadius: mouseRadius * 4, 172 speed: 4, 173 color: new THREE.Color(0xffffff) 174 }); 175 } 176 177 function init() { 178 const container = document.getElementById('container'); 179 180 scene = new THREE.Scene(); 181 scene.fog = new THREE.FogExp2(0x020108, 0.008); 182 183 camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); 184 camera.position.z = 100; 185 186 renderer = new THREE.WebGLRenderer({ antialias: true }); 187 renderer.setPixelRatio(window.devicePixelRatio); 188 renderer.setSize(window.innerWidth, window.innerHeight); 189 renderer.setClearColor(0x020108); 190 container.appendChild(renderer.domElement); 191 192 const renderScene = new RenderPass(scene, camera); 193 const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85); 194 bloomPass.threshold = 0; 195 bloomPass.strength = 1.2; 196 bloomPass.radius = 0.5; 197 198 composer = new EffectComposer(renderer); 199 composer.addPass(renderScene); 200 composer.addPass(bloomPass); 201 202 layersConfig.forEach(config => { 203 const particles = createParticleSystem(config); 204 particleLayers.push(particles); 205 scene.add(particles); 206 }); 207 208 document.addEventListener('mousemove', onMouseMove); 209 document.addEventListener('click', onClick); 210 window.addEventListener('resize', onWindowResize); 211 } 212 213 function updateParticles() { 214 mouse.lerp(targetMouse, 0.05); 215 216 ripples = ripples.filter(ripple => { 217 ripple.radius += ripple.speed; 218 ripple.strength *= 0.96; 219 return ripple.radius < ripple.maxRadius; 220 }); 221 222 particleLayers.forEach(layer => { 223 const positions = layer.geometry.attributes.position.array; 224 const colors = layer.geometry.attributes.color.array; 225 const { velocities, basePositions, baseColors, colorVelocities } = layer.userData; 226 const totalParticles = positions.length / 3; 227 228 for (let i = 0; i < totalParticles; i++) { 229 const i3 = i * 3; 230 const px = positions[i3]; 231 const py = positions[i3 + 1]; 232 const pz = positions[i3 + 2]; 233 234 let totalForce = new THREE.Vector3(); 235 let colorShift = new THREE.Vector3(); 236 237 const mouseDist = mouse.distanceTo(new THREE.Vector3(px, py, pz)); 238 if (mouseDist < mouseRadius) { 239 const forceStrength = (1 - mouseDist / mouseRadius) * 0.1; 240 const forceDirection = new THREE.Vector3(px, py, pz).sub(mouse).normalize(); 241 totalForce.add(forceDirection.multiplyScalar(forceStrength)); 242 243 const colorIntensity = (1 - mouseDist / mouseRadius) * 0.8; 244 colorShift.set(colorIntensity, colorIntensity, colorIntensity); 245 } 246 247 ripples.forEach(ripple => { 248 const rippleDist = Math.sqrt(Math.pow(ripple.x - px, 2) + Math.pow(ripple.y - py, 2)); 249 const rippleWidth = 15; 250 if (Math.abs(rippleDist - ripple.radius) < rippleWidth) { 251 const falloff = 1 - Math.abs(rippleDist - ripple.radius) / rippleWidth; 252 const rippleForce = ripple.strength * falloff * 0.1; 253 const forceDirection = new THREE.Vector3(px, py, pz).sub(new THREE.Vector3(ripple.x, ripple.y, pz)).normalize(); 254 totalForce.add(forceDirection.multiplyScalar(rippleForce)); 255 256 const rippleColor = new THREE.Vector3(ripple.color.r, ripple.color.g, ripple.color.b); 257 colorShift.add(rippleColor.multiplyScalar(falloff * ripple.strength)); 258 } 259 }); 260 261 velocities[i3] += totalForce.x; 262 velocities[i3 + 1] += totalForce.y; 263 velocities[i3 + 2] += totalForce.z; 264 265 const returnForce = 0.02; 266 velocities[i3] += (basePositions[i3] - px) * returnForce; 267 velocities[i3 + 1] += (basePositions[i3 + 1] - py) * returnForce; 268 velocities[i3 + 2] += (basePositions[i3 + 2] - pz) * returnForce; 269 270 const damping = 0.94; 271 velocities[i3] *= damping; 272 velocities[i3 + 1] *= damping; 273 velocities[i3 + 2] *= damping; 274 275 positions[i3] += velocities[i3]; 276 positions[i3 + 1] += velocities[i3 + 1]; 277 positions[i3 + 2] += velocities[i3 + 2]; 278 279 colorVelocities[i3] += colorShift.x; 280 colorVelocities[i3 + 1] += colorShift.y; 281 colorVelocities[i3 + 2] += colorShift.z; 282 283 const colorReturnForce = 0.05; 284 colorVelocities[i3] += (baseColors[i3] - colors[i3]) * colorReturnForce; 285 colorVelocities[i3 + 1] += (baseColors[i3 + 1] - colors[i3 + 1]) * colorReturnForce; 286 colorVelocities[i3 + 2] += (baseColors[i3 + 2] - colors[i3 + 2]) * colorReturnForce; 287 288 const colorDamping = 0.9; 289 colorVelocities[i3] *= colorDamping; 290 colorVelocities[i3 + 1] *= colorDamping; 291 colorVelocities[i3 + 2] *= colorDamping; 292 293 colors[i3] += colorVelocities[i3]; 294 colors[i3 + 1] += colorVelocities[i3 + 1]; 295 colors[i3 + 2] += colorVelocities[i3 + 2]; 296 } 297 298 layer.geometry.attributes.position.needsUpdate = true; 299 layer.geometry.attributes.color.needsUpdate = true; 300 }); 301 } 302 303 function animate() { 304 requestAnimationFrame(animate); 305 time += 0.01; 306 307 updateParticles(); 308 309 particleLayers.forEach(layer => { 310 layer.rotation.y += layer.userData.rotationSpeed; 311 layer.rotation.x = Math.sin(time * 0.1) * 0.05; 312 }); 313 314 camera.position.x = Math.sin(time * 0.2) * 2; 315 camera.position.y = Math.cos(time * 0.3) * 2; 316 camera.lookAt(scene.position); 317 318 composer.render(); 319 } 320 321 function onMouseMove(event) { 322 targetMouse.x = (event.clientX / window.innerWidth) * 2 - 1; 323 targetMouse.y = -(event.clientY / window.innerHeight) * 2 + 1; 324 325 const vector = new THREE.Vector3(targetMouse.x, targetMouse.y, 0.5); 326 vector.unproject(camera); 327 const dir = vector.sub(camera.position).normalize(); 328 const distance = -camera.position.z / dir.z; 329 const pos = camera.position.clone().add(dir.multiplyScalar(distance)); 330 targetMouse.copy(pos); 331 } 332 333 function onClick(event) { 334 const clickMouse = new THREE.Vector2(); 335 clickMouse.x = (event.clientX / window.innerWidth) * 2 - 1; 336 clickMouse.y = -(event.clientY / window.innerHeight) * 2 + 1; 337 338 const vector = new THREE.Vector3(clickMouse.x, clickMouse.y, 0.5); 339 vector.unproject(camera); 340 const dir = vector.sub(camera.position).normalize(); 341 const distance = -camera.position.z / dir.z; 342 const pos = camera.position.clone().add(dir.multiplyScalar(distance)); 343 344 createRipple(pos.x, pos.y); 345 } 346 347 function onWindowResize() { 348 camera.aspect = window.innerWidth / window.innerHeight; 349 camera.updateProjectionMatrix(); 350 renderer.setSize(window.innerWidth, window.innerHeight); 351 composer.setSize(window.innerWidth, window.innerHeight); 352 } 353 354 init(); 355 animate(); 356</script> 357 358 </body> 359 360</html>

HTML Breakdown


#container → WebGL mount point

.glow-overlay → CSS-based ambient glow

Import maps → Clean ES module imports

No UI clutter — full immersive canvas



🎨 Step 2: CSS – Atmosphere & Visual Styling


CSS Code working


1* { 2 margin: 0; 3 padding: 0; 4 box-sizing: border-box; 5} 6 7html, body { 8 width: 100%; 9 height: 100%; 10 overflow: hidden; 11} 12 13#container { 14 position: fixed; 15 width: 100%; 16 height: 100%; 17 background: radial-gradient(circle at 50% 50%, 18 #1a0632 0%, 19 #140426 25%, 20 #0c021a 50%, 21 #06020e 75%, 22 #020108 100% 23 ); 24} 25 26canvas { 27 display: block; 28 width: 100%; 29 height: 100%; 30} 31 32.glow-overlay { 33 position: fixed; 34 inset: 0; 35 pointer-events: none; 36 background: radial-gradient(circle at 50% 50%, 37 rgba(120, 50, 255, 0.05) 0%, 38 rgba(80, 40, 200, 0.03) 40%, 39 transparent 70%); 40 mix-blend-mode: screen; 41}

CSS Breakdown


Radial gradients → Deep space feel

Fixed canvas → Zero layout shift

Glow overlay → Enhances bloom perception

Mix-blend-mode: screen → Natural light blending



⚙️ Step 3: JavaScript – Particles, Physics & Bloom


Scene & Renderer Setup


1scene = new THREE.Scene(); 2scene.fog = new THREE.FogExp2(0x020108, 0.008); 3 4camera = new THREE.PerspectiveCamera(75, aspect, 0.1, 1000); 5camera.position.z = 100; 6 7renderer = new THREE.WebGLRenderer({ antialias: true }); 8renderer.setPixelRatio(window.devicePixelRatio); 9renderer.setSize(window.innerWidth, window.innerHeight);

Bloom Post-Processing


1const renderPass = new RenderPass(scene, camera); 2const bloomPass = new UnrealBloomPass( 3 new THREE.Vector2(window.innerWidth, window.innerHeight), 4 1.2, 0.4, 0.85 5); 6 7composer = new EffectComposer(renderer); 8composer.addPass(renderPass); 9composer.addPass(bloomPass);

Particle System Generation


1const geometry = new THREE.BufferGeometry(); 2geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); 3geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); 4 5const material = new THREE.PointsMaterial({ 6 size: 0.3, 7 vertexColors: true, 8 transparent: true, 9 blending: THREE.AdditiveBlending, 10 depthWrite: false 11}); 12 13const points = new THREE.Points(geometry, material);

Interaction Forces


Mouse Attraction

Particles repel when cursor enters radius

Force strength fades with distance


Ripple System

Click creates expanding shockwave

Particles receive outward impulse

Ripple fades naturally over time


Physics Model Used


Velocity-based movement

Spring force → returns particles to origin

Damping → prevents infinite oscillation

Color velocities → glowing pulse reactions


Animation Loop


1function animate() { 2 requestAnimationFrame(animate); 3 4 updateParticles(); 5 6 particleLayers.forEach(layer => { 7 layer.rotation.y += layer.userData.rotationSpeed; 8 }); 9 10 camera.lookAt(scene.position); 11 composer.render(); 12}

Love this component?

Explore more components and build amazing UIs.

View All Components