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
Official docs ↗

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&apos;t show WebGL, so the 3D scene is skipped.</p>}
        aria-label={label}
        {...props}
      >
        {children(animate)}
      </Canvas>
      {overlay}
    </Stage>
  );
}