React Three Fiber brings Three.js into React's component model. Here is how to build interactive 3D scenes.
Install Dependencies
pnpm add three @react-three/fiber @react-three/drei
pnpm add -D @types/three
Basic Scene
"use client";
import { Canvas } from "@react-three/fiber";
import { OrbitControls, Environment } from "@react-three/drei";
export function Scene() {
return (
<div className="h-[500px] w-full rounded-lg overflow-hidden bg-black">
<Canvas camera={{ position: [3, 3, 3], fov: 45 }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} intensity={1} castShadow />
<Box position={[0, 0.5, 0]} />
<Floor />
<OrbitControls enableDamping dampingFactor={0.05} />
<Environment preset="city" />
</Canvas>
</div>
);
}
function Box({ position }: { position: [number, number, number] }) {
return (
<mesh position={position} castShadow>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="#3b82f6" roughness={0.3} metalness={0.7} />
</mesh>
);
}
function Floor() {
return (
<mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
<planeGeometry args={[10, 10]} />
<meshStandardMaterial color="#1a1a2e" roughness={0.8} />
</mesh>
);
}
Interactive Mesh With Hover and Click
"use client";
import { useState, useRef } from "react";
import { useFrame } from "@react-three/fiber";
import type { Mesh } from "three";
export function InteractiveSphere() {
const meshRef = useRef<Mesh>(null);
const [hovered, setHovered] = useState(false);
const [clicked, setClicked] = useState(false);
useFrame((state, delta) => {
if (!meshRef.current) return;
meshRef.current.rotation.y += delta * 0.5;
// Smooth scale animation
const targetScale = clicked ? 1.5 : hovered ? 1.2 : 1;
meshRef.current.scale.lerp(
{ x: targetScale, y: targetScale, z: targetScale } as THREE.Vector3,
0.1,
);
});
return (
<mesh
ref={meshRef}
onPointerEnter={(e) => {
e.stopPropagation();
setHovered(true);
document.body.style.cursor = "pointer";
}}
onPointerLeave={() => {
setHovered(false);
document.body.style.cursor = "auto";
}}
onClick={() => setClicked((c) => !c)}
>
<sphereGeometry args={[1, 32, 32]} />
<meshStandardMaterial
color={clicked ? "#ef4444" : hovered ? "#22c55e" : "#3b82f6"}
roughness={0.2}
metalness={0.8}
/>
</mesh>
);
}
Animated Floating Objects
"use client";
import { useRef } from "react";
import { useFrame } from "@react-three/fiber";
import { Float, MeshDistortMaterial } from "@react-three/drei";
import type { Mesh } from "three";
export function FloatingObjects() {
return (
<>
<Float speed={2} rotationIntensity={1} floatIntensity={2}>
<mesh position={[-2, 1, 0]}>
<torusGeometry args={[0.8, 0.3, 16, 32]} />
<MeshDistortMaterial
color="#a855f7"
speed={3}
distort={0.3}
roughness={0.2}
/>
</mesh>
</Float>
<Float speed={1.5} rotationIntensity={2} floatIntensity={1.5}>
<mesh position={[2, 1, 0]}>
<octahedronGeometry args={[0.8]} />
<meshStandardMaterial
color="#eab308"
roughness={0.1}
metalness={0.9}
/>
</mesh>
</Float>
<AnimatedRing position={[0, 2, -1]} />
</>
);
}
function AnimatedRing({ position }: { position: [number, number, number] }) {
const ref = useRef<Mesh>(null);
useFrame((state) => {
if (!ref.current) return;
ref.current.rotation.x = Math.sin(state.clock.elapsedTime) * 0.3;
ref.current.rotation.z = Math.cos(state.clock.elapsedTime * 0.5) * 0.2;
});
return (
<mesh ref={ref} position={position}>
<torusGeometry args={[1.2, 0.05, 16, 64]} />
<meshStandardMaterial color="#22c55e" metalness={1} roughness={0} />
</mesh>
);
}
Product Viewer
"use client";
import { Suspense } from "react";
import { Canvas } from "@react-three/fiber";
import {
OrbitControls,
Environment,
ContactShadows,
Html,
useGLTF,
} from "@react-three/drei";
interface ProductViewerProps {
modelUrl: string;
environmentPreset?: "city" | "sunset" | "studio" | "warehouse";
}
export function ProductViewer({
modelUrl,
environmentPreset = "studio",
}: ProductViewerProps) {
return (
<div className="h-[400px] w-full rounded-lg overflow-hidden bg-gradient-to-b from-gray-100 to-gray-200">
<Canvas camera={{ position: [0, 1, 3], fov: 40 }}>
<Suspense
fallback={
<Html center>
<div className="text-sm text-muted-foreground">Loading 3D model...</div>
</Html>
}
>
<Model url={modelUrl} />
<Environment preset={environmentPreset} />
</Suspense>
<ContactShadows
position={[0, -0.5, 0]}
opacity={0.4}
blur={2}
far={4}
/>
<OrbitControls
enablePan={false}
minDistance={2}
maxDistance={6}
minPolarAngle={Math.PI / 6}
maxPolarAngle={Math.PI / 2}
/>
</Canvas>
</div>
);
}
function Model({ url }: { url: string }) {
const { scene } = useGLTF(url);
return <primitive object={scene} scale={1} />;
}
Responsive Canvas With Lazy Loading
"use client";
import dynamic from "next/dynamic";
import { Suspense } from "react";
// Lazy load the entire 3D scene
const Scene3D = dynamic(
() => import("./Scene3D").then((mod) => mod.Scene3D),
{
ssr: false,
loading: () => (
<div className="h-[500px] w-full bg-muted rounded-lg flex items-center justify-center">
<span className="text-muted-foreground text-sm">Loading 3D scene...</span>
</div>
),
},
);
export function LazyScene() {
return (
<Suspense fallback={null}>
<Scene3D />
</Suspense>
);
}
Performance Tips
// 1. Use instances for repeated geometry
import { Instances, Instance } from "@react-three/drei";
function ParticleField({ count = 1000 }) {
return (
<Instances limit={count} range={count}>
<sphereGeometry args={[0.02, 8, 8]} />
<meshBasicMaterial color="#ffffff" />
{Array.from({ length: count }, (_, i) => (
<Instance
key={i}
position={[
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
]}
/>
))}
</Instances>
);
}
// 2. Use frameloop="demand" for static scenes
<Canvas frameloop="demand">
{/* Only re-renders when something changes */}
</Canvas>
// 3. Use LOD for performance
import { Detailed } from "@react-three/drei";
function AdaptiveModel() {
return (
<Detailed distances={[0, 10, 25]}>
<HighDetailModel />
<MediumDetailModel />
<LowDetailModel />
</Detailed>
);
}
Need 3D Web Experiences?
We build immersive 3D experiences for products and brands. Contact us to discuss your project.