EmpireUI
Get Pro
← Blog8 min read#three.js#react#3d

Three.js with React: Particles, Blobs and Interactive 3D Scenes

Learn how to build interactive 3D scenes in React using react-three-fiber — particles, animated blobs, and GPU-powered WebGL effects without raw Three.js boilerplate.

Colorful 3D particle cloud rendered in a dark browser WebGL canvas

Why react-three-fiber Instead of Raw Three.js

Three.js is incredible. It's also an imperative, mutation-heavy API that doesn't naturally fit inside a React component tree. You end up managing scene lifecycles in useEffect, keeping refs to every mesh, and manually calling renderer.render() in animation loops — and that's before you've done anything interesting.

react-three-fiber (r3f) wraps Three.js in a declarative reconciler so you can write JSX that compiles down to Three.js objects. Released back in 2019, it's now at v8 and stable enough for production. Every Three.js class becomes a JSX tag: <mesh>, <pointLight>, <sphereGeometry>. You get React's state, hooks, and context for free.

In practice, this means your 3D scene and your UI live in the same component model. You can pass props into geometry, wire up onClick handlers on meshes, and animate via useFrame — no separate game loop needed. Honestly, if you've been avoiding WebGL because of the setup cost, r3f makes it approachable without hiding what's actually happening.

That said, you still need to understand Three.js fundamentals. r3f doesn't abstract away materials, cameras, or the scene graph — it just lets you express them in JSX. The docs at pmndrs.github.io are worth an afternoon.

Setting Up Your First Canvas

Install three packages: three, @react-three/fiber, and @react-three/drei. Drei is a helper library from the same team — it gives you things like OrbitControls, Environment maps, and Text3D without rebuilding them every project.

npm install three @react-three/fiber @react-three/drei

Your entry point is the <Canvas> component. Drop it into any React component and it creates a WebGL renderer, a default camera at z=5, and a resize observer. Everything inside Canvas is rendered in 3D space — not the DOM.

import { Canvas } from '@react-three/fiber'
import { OrbitControls } from '@react-three/drei'

export default function Scene() {
  return (
    <div style={{ width: '100vw', height: '100vh' }}>
      <Canvas camera={{ fov: 60, position: [0, 0, 5] }}>
        <ambientLight intensity={0.5} />
        <pointLight position={[10, 10, 10]} />
        <mesh>
          <sphereGeometry args={[1, 64, 64]} />
          <meshStandardMaterial color="#7c3aed" />
        </mesh>
        <OrbitControls />
      </Canvas>
    </div>
  )
}

Quick aside: the args prop on geometry constructors maps directly to the Three.js constructor arguments. <sphereGeometry args={[1, 64, 64]} /> is new THREE.SphereGeometry(1, 64, 64). Once you know that pattern, reading Three.js docs and writing r3f code feel interchangeable.

Animating Blobs with useFrame and Shader Materials

Static meshes are fine for product demos, but the real appeal of WebGL is animation. useFrame gives you a callback that fires every frame — synced to the display's refresh rate, not a setInterval. You get access to the current clock time and the Three.js renderer state.

For organic blob shapes, you'd typically animate vertex positions in a custom shader or use a library like maath to perturb geometry each frame. Here's a simple rotation + scale pulse using just useFrame and a ref:

import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'

function AnimatedBlob() {
  const meshRef = useRef()

  useFrame(({ clock }) => {
    const t = clock.getElapsedTime()
    meshRef.current.rotation.x = Math.sin(t * 0.4) * 0.3
    meshRef.current.rotation.y = t * 0.2
    // subtle scale pulse
    const s = 1 + Math.sin(t * 1.5) * 0.05
    meshRef.current.scale.setScalar(s)
  })

  return (
    <mesh ref={meshRef}>
      <icosahedronGeometry args={[1.2, 4]} />
      <meshStandardMaterial
        color="#06b6d4"
        wireframe={false}
        roughness={0.2}
        metalness={0.6}
      />
    </mesh>
  )
}

For true vertex displacement you'd write a <shaderMaterial> with custom GLSL — but that's a separate article. The icosahedron at subdivision level 4 already gives you a smooth sphere-like blob with enough polygon density to look good with normal-mapped materials.

Worth noting: useFrame runs inside the Canvas context, so it won't work if you accidentally call it outside a Canvas tree. That's the most common gotcha beginners hit.

Building a Particle System from Scratch

Particles are where WebGL genuinely pulls ahead of CSS. Ten thousand animated points? No problem. In Three.js you'd use BufferGeometry with a Float32Array for positions and a PointsMaterial. In r3f it's the same idea — just declarative.

import { useMemo, useRef } from 'react'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'

function Particles({ count = 5000 }) {
  const pointsRef = useRef()

  const positions = useMemo(() => {
    const arr = new Float32Array(count * 3)
    for (let i = 0; i < count; i++) {
      arr[i * 3] = (Math.random() - 0.5) * 10
      arr[i * 3 + 1] = (Math.random() - 0.5) * 10
      arr[i * 3 + 2] = (Math.random() - 0.5) * 10
    }
    return arr
  }, [count])

  useFrame(({ clock }) => {
    pointsRef.current.rotation.y = clock.getElapsedTime() * 0.05
  })

  return (
    <points ref={pointsRef}>
      <bufferGeometry>
        <bufferAttribute
          attach="attributes-position"
          count={count}
          array={positions}
          itemSize={3}
        />
      </bufferGeometry>
      <pointsMaterial
        size={0.02}
        color="#a78bfa"
        sizeAttenuation
        transparent
        opacity={0.8}
      />
    </points>
  )
}

The useMemo is non-negotiable here. Recomputing 5000 random positions on every render would kill your framerate. Generate once, keep the reference stable.

Honestly, a dark canvas with 5000 purple particles slowly rotating looks stunning as a hero background — and the GPU barely breaks a sweat. Pair it with the kind of layered depth effects you'll find in Empire UI's glassmorphism components and you've got a hero section that actually earns attention.

Raycasting and Mouse Interaction

Making things interactive is where the fun starts. r3f handles raycasting automatically — you can add onClick, onPointerOver, onPointerOut directly to any mesh, just like DOM events. The library fires synthetic Three.js raycaster events under the hood so you never write that boilerplate yourself.

function InteractiveSphere() {
  const [hovered, setHovered] = useState(false)

  return (
    <mesh
      onPointerOver={() => setHovered(true)}
      onPointerOut={() => setHovered(false)}
    >
      <sphereGeometry args={[0.8, 32, 32]} />
      <meshStandardMaterial
        color={hovered ? '#f59e0b' : '#7c3aed'}
        roughness={0.3}
      />
    </mesh>
  )
}

That's a 200px sphere that changes color on hover. No manual raycasting, no mouse coordinate transforms. Worth noting that r3f uses pointer events, not mouse events, so it works on touch devices too.

For more advanced interaction — dragging, constraint systems, physics-based hover effects — check out @react-three/rapier for physics and @use-gesture/react for gesture binding. The pmndrs ecosystem is huge and most of it plays nicely together. One more thing — if you want cursor-level polish on your 3D scenes, it's worth checking out what Empire UI does with custom cursors to see how interaction design can extend into the WebGL layer.

Performance: What to Watch Out For

Three.js scenes can tank your framerate fast if you're not careful. The two biggest offenders are geometry creation inside render loops and material duplication. Every new THREE.MeshStandardMaterial() allocates GPU memory. Create materials once and reuse them.

r3f's <Instances> component solves repeated geometry efficiently using instanced rendering — one draw call for thousands of identical meshes. If you're building a particle system with custom shapes rather than points, reach for Instances rather than individual meshes.

On mobile, target 30fps as a baseline. Drop antialias from the Canvas renderer props, reduce polygon counts, and avoid expensive post-processing effects on low-end devices. You can detect GPU tier using the detect-gpu package and conditionally render lighter scenes. Look, a gorgeous 3D hero that drops to 10fps on a mid-range Android isn't a feature — it's a bug.

The gradient generator and other tools on Empire UI are good examples of how to keep GPU work scoped — effects stay where they're needed, not on the whole page.

Post-Processing and Visual Polish

@react-three/postprocessing wraps postprocessing (the fast alternative to Three.js EffectComposer) in r3f-friendly components. You get bloom, depth of field, chromatic aberration, and noise effects with a few JSX lines.

import { EffectComposer, Bloom, ChromaticAberration } from '@react-three/postprocessing'
import { BlendFunction } from 'postprocessing'

// Inside your Canvas:
<EffectComposer>
  <Bloom
    luminanceThreshold={0.2}
    luminanceSmoothing={0.9}
    intensity={1.5}
  />
  <ChromaticAberration
    blendFunction={BlendFunction.NORMAL}
    offset={[0.002, 0.002]}
  />
</EffectComposer>

Bloom at intensity 1.5 with a luminance threshold of 0.2 is a good starting point for a glowing particle scene. Push it higher and you get the oversaturated neon look that pairs well with cyberpunk or vaporwave aesthetics.

Post-processing has a real GPU cost. Profile with Chrome DevTools' rendering panel before shipping. A scene that looks great at 144fps on your M3 MacBook might struggle at 60fps on a 2021 Windows laptop. Test on real hardware.

FAQ

Do I need to know Three.js before using react-three-fiber?

You don't need to be an expert, but understanding Three.js concepts like scenes, cameras, geometries, and materials will save you a lot of confusion. r3f maps directly to Three.js — it's JSX syntax on top, not a new abstraction.

Can I use react-three-fiber in a Next.js project?

Yes, but Canvas must render client-side only. Wrap it in a dynamic import with ssr: false or use the 'use client' directive in Next 13+. WebGL requires a browser environment.

How do I handle responsive sizing for a 3D canvas?

The Canvas component handles resize automatically — it observes its parent container. Just make sure the parent has explicit width and height via CSS, and you're set.

What's the difference between @react-three/fiber and @react-three/drei?

r3f is the core renderer — it maps JSX to Three.js objects. Drei is a collection of helpers built on top of r3f: controls, loaders, shaders, and abstractions you'd otherwise write yourself. Use both together.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

React Three Fiber in 2026: 3D Scenes, Shaders and Performancereact-three-fiber + Drei: Cameras, Controls, Helpers and Loadersreact-three-fiber Intro: 3D Scenes in React With Three.jsReact Three Fiber: 3D Graphics in React Without WebGL Expertise