@react-three/drei
@react-three/drei is the standard helper collection for @react-three/fiber, React's renderer for Three.js. It packages cameras and controls, Suspense-based model and texture loaders, HTML and text overlays, environment lighting, staging, shadows, instancing, performance tools, gizmos, materials, portals, and many small hooks as typed React components. It does not replace Three.js or Fiber; it sits on both and makes common scene work declarative.
Drei is the sensible default companion for a React Three Fiber application, provided you import selectively and treat loaders, shadows, environments, and DOM overlays as real systems with costs. Do not add it to plain Three.js, an older React/Fiber stack, or a tiny scene that needs only one trivial helper.
Use it if
- You already use React 19, @react-three/fiber 9, and Three.js, and want maintained declarative helpers instead of wrapping examples yourself
- Your scenes repeatedly need GLTF loading, camera controls, environment maps, text, HTML annotations, bounds fitting, or instancing
- You value components that clean up controls on unmount, work with demand rendering, and share conventions across a React 3D team
- You can profile component-specific GPU, CPU, asset, and network costs instead of assuming every helper is cheap
- You are not using @react-three/fiber: Drei components depend on Fiber context and are not a general Three.js utility library
- Your app is still on React 18 or Fiber 8: current 10.7.8 peers require React 19 and @react-three/fiber 9, so install a compatible older Drei line or upgrade the renderer stack together
- You need a minimal dependency and audit surface: the package declares 21 runtime dependencies, including media, text, controls, BVH, GPU detection, state, and vision helpers, and Bundlephobia measures the complete package at 499.9 KB gzipped before your own Three.js and React stack
- You target React Native expecting feature parity: the README says the /native route omits Html and Loader, while the default entry is the web build
- You need fully local, predictable assets and fixed frame costs without configuration: useGLTF defaults to Google-hosted Draco decoders, Environment presets rely on CDNs and are explicitly discouraged for production, ContactShadows is described as expensive, and declarative Instances adds CPU overhead
Setup reality
Install @react-three/drei alongside its required peers: react ^19, @react-three/fiber ^9.0.0, and three >=0.159; react-dom ^19 is an optional peer but web helpers such as Html and Loader need the DOM. The package includes TypeScript declarations, CommonJS and ESM entry files, and marks itself side-effect free for tree shaking. Use named imports and measure the built application because installing Drei still brings 21 runtime dependencies and importing broad barrels or feature-heavy helpers can pull substantial code. Components and hooks that access Fiber state belong under Canvas, and asset loaders suspend, so provide a React Suspense boundary or a loading overlay. WebGL and DOM helpers also need a client-side boundary in server-rendered frameworks. Asset URLs must be deployed and CORS-readable; GLTF textures and decoder files are separate network concerns. useGLTF enables Draco support by default and downloads decoders from Google's CDN only when a compressed model needs them. Set a self-hosted decoder path when offline operation, content-security policy, or supply control matters. Environment preset names fetch remote HDR files and the docs say not to depend on them in production; self-host files instead. Drei uses three-stdlib rather than three/examples/jsm, so import matching loaders and controls from three-stdlib when extending its internals. For React Native, import @react-three/drei/native and accept the missing Html and Loader exports plus the platform setup required by Fiber native. Many helpers create render targets, event handlers, observers, or per-frame updates; convenience does not remove GPU budgets, disposal concerns, or the need to test mobile hardware.
Patterns
Add controls to a Fiber canvasadd-orbit-controls
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
export function Scene() {
return (
<Canvas camera={{ position: [0, 2, 5], fov: 50 }}>
<mesh>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
<OrbitControls makeDefault enableDamping />
</Canvas>
);
}Drei controls must live under Canvas. makeDefault stores the controls in Fiber so Bounds, camera shake, gizmos, and other helpers can coordinate with them.
Center, light, and frame a modelstage-model
<Canvas shadows>
<Stage
adjustCamera={1.2}
intensity={0.5}
shadows="contact"
environment={{ files: '/hdr/studio.hdr' }}
>
<Model />
</Stage>
<OrbitControls makeDefault />
</Canvas>Stage combines several helpers and defaults to a remote city environment when given only a preset name. Pass self-hosted environment files for production and use makeDefault controls with adjustCamera.
Load a GLTF model with Suspenseload-gltf
import { Suspense } from 'react';
import { useGLTF } from '@react-three/drei';
function Model(props) {
const { scene } = useGLTF('/models/chair.glb');
return <primitive object={scene} {...props} />;
}
<Suspense fallback={null}>
<Model />
</Suspense>useGLTF suspends while loading and caches by URL. The returned scene is one object graph; use Drei's Clone or a cloned graph when mounting repeated independent copies.
Self-host Draco and preload a modelpreload-gltf
import { useGLTF } from '@react-three/drei';
useGLTF.setDecoderPath('/draco/');
useGLTF.preload('/models/compressed.glb');
function Model() {
const gltf = useGLTF('/models/compressed.glb', '/draco/');
return <primitive object={gltf.scene} />;
}Draco decoding defaults to binaries on Google's CDN. Self-host the matching decoder files for offline use and strict content-security policies; preload starts network work before render.
Load named material texturesload-material-textures
const maps = useTexture({
map: '/textures/albedo.jpg',
normalMap: '/textures/normal.jpg',
roughnessMap: '/textures/roughness.jpg',
});
return <meshStandardMaterial {...maps} roughness={1} />;useTexture suspends and returns keys matching the input object. Configure color space, wrapping, repeat, and anisotropy explicitly when the texture's role requires them.
Use a self-hosted lighting environmentset-environment
<Suspense fallback={null}>
<Environment
files="/environments/studio.hdr"
background={false}
environmentIntensity={0.8}
/>
</Suspense>Named presets fetch CDN assets and the docs say they may fail in production. Self-host HDR, EXR, gain-map, or cube-map files and include their download cost in loading UX.
Attach an occluded DOM label to 3Dattach-html-label
<mesh ref={targetRef} position={[0, 1, 0]}>
<sphereGeometry args={[0.5, 32, 32]} />
<meshStandardMaterial />
<Html center occlude={[targetRef]} distanceFactor={8}>
<button className="label">Details</button>
</Html>
</mesh>Html is web-only and needs react-dom. Occlusion adds raycasting work, transform mode can look blurry on some devices, and blending occlusion is rectangular unless you supply geometry.
Render crisp 3D text without a flashrender-sdf-text
<Text
font="/fonts/Inter-Regular.woff"
fontSize={0.4}
color="white"
anchorX="center"
anchorY="middle"
characters="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
>
SCORE 1200
</Text>Text uses Troika SDF rendering and suspends for font data. Supplying the needed characters lets glyph generation happen before display and avoids a flash of missing content.
Fit and clip the camera to scene boundsfit-camera-to-content
<Bounds fit clip observe margin={1.2} maxDuration={1}>
<group>
<Model />
</group>
</Bounds>
<OrbitControls makeDefault />Bounds calculation can be expensive, so call useBounds().refresh() only when content changes. Default controls must be registered for coordinated camera motion.
Render repeated meshes in one draw callinstance-repeated-meshes
<Instances limit={500} range={items.length}>
<boxGeometry />
<meshStandardMaterial />
{items.map((item) => (
<Instance
key={item.id}
position={item.position}
color={item.color}
onClick={() => select(item.id)}
/>
))}
</Instances>limit allocates the instance buffers and must cover the maximum. Declarative Instance components reduce draw calls but the docs warn about CPU overhead for very large static populations such as foliage.
Render static contact shadows oncelimit-contact-shadows
<ContactShadows
position={[0, -0.01, 0]}
opacity={0.7}
scale={10}
blur={1.5}
far={8}
resolution={256}
frames={1}
/>ContactShadows is explicitly described as expensive. frames={1} is appropriate only when shadow-casting objects and lighting stay static.
Display shared Three.js loading progressshow-loading-progress
function LoadingOverlay() {
const progress = useProgress((state) => state.progress);
return <Html fullscreen center>{Math.round(progress)}% loaded</Html>;
}
<Suspense fallback={<LoadingOverlay />}>
<AsyncScene />
</Suspense>useProgress wraps THREE.DefaultLoadingManager, so it reports assets using that manager rather than every fetch in your app. A selector avoids re-rendering for unrelated loading-state fields.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @react-three/fiber | npm | Use Fiber alone and write a few local wrappers when only one or two Drei conveniences are needed |
| three | npm | Use imperative Three.js when React reconciliation and component abstractions do not fit the render architecture |
| three-stdlib | npm | Use maintained examples-derived controls and loaders directly when building your own Fiber components |
| @react-three/postprocessing | npm | Use the narrower effects package when postprocessing is the only higher-level feature the scene needs |