three is the core WebGL library that provides the low-level API for rendering 3D graphics in the browser. @react-three/fiber is a React renderer for three, allowing developers to build 3D scenes using declarative React components instead of imperative code. @react-three/drei is a collection of useful helpers and abstractions built on top of @react-three/fiber, offering ready-made components for controls, loaders, and common 3D features.
Building 3D experiences on the web usually starts with three, but React developers often reach for @react-three/fiber and @react-three/drei. These tools solve different problems in the same stack. Let's compare how they handle core tasks.
three requires manual setup of the scene, camera, and renderer. You manage the DOM element and the render loop yourself.
// three: Manual setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(...);
const renderer = new THREE.WebGLRenderer({ canvas });
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
@react-three/fiber wraps this logic in a <Canvas> component. It handles the loop and resize events automatically.
// @react-three/fiber: Declarative Canvas
import { Canvas } from '@react-three/fiber';
function App() {
return <Canvas><Scene /></Canvas>;
}
@react-three/drei does not replace the renderer. It assumes you are already inside a Fiber <Canvas>. You use it to add features within that context.
// @react-three/drei: Works inside Fiber Canvas
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
function App() {
return <Canvas><OrbitControls /></Canvas>;
}
three uses imperative methods to add objects. You must manually track references to remove or update them.
// three: Imperative addition
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// Later...
scene.remove(mesh);
@react-three/fiber uses JSX components. Adding or removing objects is handled by React's reconciliation.
// @react-three/fiber: JSX components
function Scene() {
return <mesh geometry={geometry} material={material} />;
}
@react-three/drei provides complex scene objects as ready-made components. This saves you from writing boilerplate for common items like text or shadows.
// @react-three/drei: Pre-built components
import { Text, ContactShadows } from '@react-three/drei';
function Scene() {
return (
<>
<Text>Hello</Text>
<ContactShadows />
</>
);
}
three relies on requestAnimationFrame. You update positions directly inside the loop function.
// three: Manual loop
function animate() {
requestAnimationFrame(animate);
mesh.rotation.x += 0.01;
renderer.render(scene, camera);
}
@react-three/fiber provides the useFrame hook. It gives you access to the state and clock without managing the loop yourself.
// @react-three/fiber: useFrame hook
import { useFrame } from '@react-three/fiber';
function Box() {
const ref = useRef();
useFrame((state, delta) => (ref.current.rotation.x += delta));
return <mesh ref={ref} />;
}
@react-three/drei offers specialized hooks for specific animation tasks, like animating GLTF models.
// @react-three/drei: useAnimations hook
import { useAnimations, useGLTF } from '@react-three/drei';
function Model() {
const { scene } = useGLTF('/model.glb');
const { actions } = useAnimations(animations, scene);
useEffect(() => { actions['Walk'].play(); }, [actions]);
return <primitive object={scene} />;
}
three requires importing separate control classes (like OrbitControls) and wiring them to the camera and DOM events manually.
// three: Manual controls wiring
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
const controls = new OrbitControls(camera, renderer.domElement);
controls.addEventListener('change', () => renderer.render(scene, camera));
@react-three/fiber allows you to write custom event handlers on 3D objects using standard React props like onClick.
// @react-three/fiber: React events
<mesh onClick={(e) => console.log('clicked')} />
@react-three/drei includes popular controls as components. You drop them into the scene graph instead of wiring them manually.
// @react-three/drei: Componentized controls
import { OrbitControls } from '@react-three/drei';
function Scene() {
return <OrbitControls makeDefault />;
}
three uses loaders like GLTFLoader. You must manage loading states and errors manually.
// three: Manual loading
const loader = new GLTFLoader();
loader.load('/model.glb', (gltf) => scene.add(gltf.scene));
@react-three/fiber handles asset loading via the useLoader hook, suspending the component until the asset is ready.
// @react-three/fiber: useLoader hook
import { useLoader } from '@react-three/fiber';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
function Model() {
const gltf = useLoader(GLTFLoader, '/model.glb');
return <primitive object={gltf.scene} />;
}
@react-three/drei simplifies this further with useGLTF, which includes caching and preloading features out of the box.
// @react-three/drei: useGLTF helper
import { useGLTF } from '@react-three/drei';
function Model() {
const { scene } = useGLTF('/model.glb');
return <primitive object={scene} />;
}
| Feature | three | @react-three/fiber | @react-three/drei |
|---|---|---|---|
| Style | Imperative | Declarative (React) | Helper Components |
| Renderer | Manual Setup | <Canvas> Component | Uses Fiber Canvas |
| State | Manual Management | React State / Context | Hooks & Context |
| Controls | Manual Wiring | React Events | Pre-built Components |
| Loading | Manual Loaders | useLoader | useGLTF + Cache |
three is the engine. Use it if you are not using React or need low-level control over every frame without React's overhead.
@react-three/fiber is the bridge. Use it to build 3D apps in React. It lets you treat 3D objects like DOM elements.
@react-three/drei is the toolbox. Use it with Fiber to save time on common tasks like controls, shadows, and loading.
Final Thought: These tools are not rivals — they are layers. drei sits on fiber, which sits on three. Choose the layer that matches your project needs.
Choose three if you are working outside the React ecosystem or need complete imperative control over the WebGL render loop without React's reconciliation overhead. It is best for custom engines, non-React projects, or when you need to optimize every millisecond of the animation frame.
Choose @react-three/drei when you are already using @react-three/fiber and want to avoid reinventing common wheels like camera controls, environment maps, or model loading. It saves development time by providing battle-tested components that work seamlessly within the Fiber ecosystem.
Choose @react-three/fiber if you are building a React application and want to manage 3D scenes using components, hooks, and state. It is ideal for integrating 3D content with standard React UI, handling complex state updates, and leveraging the React developer tools.
The aim of the project is to create an easy-to-use, lightweight, cross-browser, general-purpose 3D library. The current builds only include WebGL and WebGPU renderers but SVG and CSS3D renderers are also available as addons.
Examples — Docs — Manual — Wiki — Migrating — Questions — Forum — Discord
This code creates a scene, a camera, and a geometric cube, and it adds the cube to the scene. It then creates a WebGL renderer for the scene and camera, and it adds that viewport to the document.body element. Finally, it animates the cube within the scene for the camera.
import * as THREE from 'three';
const width = window.innerWidth, height = window.innerHeight;
// init
const camera = new THREE.PerspectiveCamera( 70, width / height, 0.01, 10 );
camera.position.z = 1;
const scene = new THREE.Scene();
const geometry = new THREE.BoxGeometry( 0.2, 0.2, 0.2 );
const material = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( width, height );
renderer.setAnimationLoop( animate );
document.body.appendChild( renderer.domElement );
// animation
function animate( time ) {
mesh.rotation.x = time / 2000;
mesh.rotation.y = time / 1000;
renderer.render( scene, camera );
}
If everything goes well, you should see this.
Cloning the repo with all its history results in a ~2 GB download. If you don't need the whole history you can use the depth parameter to significantly reduce download size.
git clone --depth=1 https://github.com/mrdoob/three.js.git