A first 3D scene
Camera, lights, meshes and controls, written as React components with react-three-fiber.
What it is
Three.js draws 3D with WebGL: a scene of meshes (geometry + material), lit by lights and seen through a camera. react-three-fiber lets you write that scene as JSX (<mesh>, <directionalLight>) and runs your code every frame with useFrame. drei adds ready-made pieces such as <OrbitControls>.
When to use it
- Product viewers and configurators
- Data visualisation in 3D
- Hero scenes and interactive backgrounds
Drag to orbit, scroll to zoom
Loading 3D scene…
With motion reduced or the scene off screen, it stops spinning and only redraws when you move the camera (frameloop="demand", set by the shared ThreeCanvas). Turn on Show FPS in the sidebar to see the difference.
Show code · src/app/three-basics/Scene.tsx
"use client";
import { OrbitControls } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";
import { useRef } from "react";
import type { Mesh } from "three";
import { useMotion } from "@/components/motion/MotionProvider";
import { ThreeCanvas } from "@/components/three/ThreeCanvas";
// A mesh is geometry (its shape) plus a material (how light hits it).
// useFrame runs every rendered frame; `delta` is the seconds since the last
// one, so the spin speed doesn't depend on the screen's refresh rate.
function Spinner({ spin }: { spin: boolean }) {
const mesh = useRef<Mesh>(null);
useFrame((_, delta) => {
if (spin && mesh.current) {
mesh.current.rotation.x += delta * 0.4;
mesh.current.rotation.y += delta * 0.6;
}
});
return (
<mesh ref={mesh} castShadow position={[0, 1.1, 0]}>
<torusKnotGeometry args={[0.7, 0.25, 160, 24]} />
<meshStandardMaterial color="#7b6cf0" roughness={0.3} metalness={0.2} />
</mesh>
);
}
export function Scene() {
const { reduced } = useMotion();
return (
<ThreeCanvas shadows camera={{ position: [3, 2.5, 4], fov: 45 }} label="A spinning torus knot on a floor. Drag to orbit the camera.">
{(animate) => (
<>
<color attach="background" args={["#14131a"]} />
<ambientLight intensity={0.35} />
<directionalLight position={[4, 6, 3]} intensity={2} castShadow shadow-mapSize={[1024, 1024]} />
<Spinner spin={animate} />
<mesh rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
<planeGeometry args={[12, 12]} />
<meshStandardMaterial color="#2a2836" />
</mesh>
<OrbitControls enableDamping={!reduced} minDistance={2.5} maxDistance={10} maxPolarAngle={Math.PI / 2.1} />
</>
)}
</ThreeCanvas>
);
}
Show code · src/components/three/ThreeCanvas.tsx
"use client";
import { Canvas, type CanvasProps } from "@react-three/fiber";
import { useRef, type ReactNode } from "react";
import { useMotion } from "@/components/motion/MotionProvider";
import { useInView } from "@/components/motion/useInView";
import { Stage } from "@/components/ui";
// The frame every 3D demo uses. It renders continuously only while the scene
// is on screen and motion isn't reduced; otherwise frameloop "demand" redraws
// only when something changes (dragging the camera, a control). Children get
// `animate` to know whether to move things themselves.
export function ThreeCanvas({
label,
className = "h-95",
overlay,
children,
...props
}: Omit<CanvasProps, "children" | "frameloop" | "fallback"> & {
label: string;
className?: string;
/** DOM laid over the canvas, inside the same React tree (so context works). */
overlay?: ReactNode;
children: (animate: boolean) => ReactNode;
}) {
const { reduced } = useMotion();
const wrapper = useRef<HTMLDivElement>(null);
const animate = useInView(wrapper) && !reduced;
return (
<Stage ref={wrapper} className={className}>
<Canvas
frameloop={animate ? "always" : "demand"}
fallback={<p className="p-4 text-muted">This browser can't show WebGL, so the 3D scene is skipped.</p>}
aria-label={label}
{...props}
>
{children(animate)}
</Canvas>
{overlay}
</Stage>
);
}