You load a glTF model, the code runs without crashing, and all you get is a black or completely blank canvas. When a Three.js model is not showing, the cause is almost always one of a short list: the file never loaded, the model is the wrong size or in the wrong place for the camera, it has no light, or the canvas itself is 0 pixels tall. This guide walks through each one in the order you should check them.
Quick fix
Most of the time, the model loaded but the camera can’t see it. Run these four checks:
- Open the console. A
404, a CORS error, orTHREE.GLTFLoader: No DRACOLoader instance providedmeans the model never loaded. Fix that first. - Log the bounding box once the model loads, then centre the model and move the camera so it fits in view (code below).
- Add light.
MeshStandardMaterialandMeshPhysicalMaterialrender black without lights or an environment map. - Check the canvas size. If
renderer.domElement.clientHeightis0, the container has no height.
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
loader.load(
'/models/product.glb',
(gltf) => {
const model = gltf.scene;
scene.add(model);
frameObject(model, camera, controls);
console.log('size', new THREE.Box3().setFromObject(model).getSize(new THREE.Vector3()));
},
undefined,
(error) => console.error('Model failed to load:', error)
);
function frameObject(object, camera, controls) {
const box = new THREE.Box3().setFromObject(object);
const size = box.getSize(new THREE.Vector3());
const center = box.getCenter(new THREE.Vector3());
object.position.sub(center); // move the model's centre to the origin
const maxDim = Math.max(size.x, size.y, size.z);
const fov = THREE.MathUtils.degToRad(camera.fov);
const distance = (maxDim / 2) / Math.tan(fov / 2) * 1.5;
camera.position.set(0, maxDim * 0.25, distance);
camera.near = distance / 100;
camera.far = distance * 100;
camera.updateProjectionMatrix();
camera.lookAt(0, 0, 0);
if (controls) {
controls.target.set(0, 0, 0);
controls.update();
}
}
Cause 1: the file never loaded (404, CORS or wrong path)
Always pass an error callback to loader.load(). Without one, a failed load can fail silently and leave you staring at an empty scene. The usual culprits:
- Wrong path. In Vite and Next.js, files in
public/are served from the site root, sopublic/models/chair.glbbecomes/models/chair.glb, not/public/models/chair.glb. - A 404 that looks like a 200. SPA hosts often answer unknown URLs with
index.html. The loader then tries to parse HTML and throws something likeUnexpected token '<'. When you see that error, the URL is wrong. - Missing external files. A
.gltffile references a.binfile and textures by relative path. Upload all of them, or export a single.glbinstead. - CORS. Models on a CDN or bucket need an
Access-Control-Allow-Originheader. See how to fix CORS errors.
Cause 2: the model is too small, too big or far from the origin
glTF uses metres. A model exported in millimetres comes in 1,000 times too large, and your camera ends up inside it. A model exported in centimetres can be too small to see at all. CAD and architecture exports often sit thousands of units from the origin, because they keep their real-world coordinates.
The frameObject() helper above deals with all three: it measures the model with Box3, moves it to the origin and places the camera at a distance that fits its largest dimension. If you need a fixed size, for example in a product configurator, scale the model to a known size instead:
const size = new THREE.Box3().setFromObject(model).getSize(new THREE.Vector3());
const target = 2; // world units
model.scale.setScalar(target / Math.max(size.x, size.y, size.z));
Cause 3: camera near/far planes or a camera inside the model
Anything closer than camera.near or farther than camera.far is clipped. A camera at (0, 0, 0) with a model also at the origin sits inside the mesh. Back-face culling then hides the inside faces, so you see nothing. Move the camera back, and keep the near/far ratio sensible. A tiny near such as 0.00001 also causes z-fighting. Always call camera.updateProjectionMatrix() after changing fov, near, far or aspect.
Cause 4: no lights, so the model renders black
A pure black silhouette on a white page means the model is there but unlit. glTF materials load as MeshStandardMaterial or MeshPhysicalMaterial, and they need light. Metallic surfaces especially need an environment map, because they mostly reflect their surroundings. Point lights alone often leave them dark.
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
const pmrem = new THREE.PMREMGenerator(renderer);
scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 1));
const sun = new THREE.DirectionalLight(0xffffff, 2);
sun.position.set(5, 10, 7);
scene.add(sun);
If the model shows up but the colours look flat or wrong, that’s a colour-management problem rather than a loading one. See how to fix washed-out Three.js colours.
Cause 5: the canvas is 0 pixels tall
This is the most common cause in React and react-three-fiber projects. <Canvas> fills its parent, and a parent div with no explicit height is 0 pixels tall. In vanilla Three.js, renderer.setSize(container.clientWidth, container.clientHeight) does the same thing when the container is empty. Give the container a real height:
<div style={{ width: '100%', height: '100vh' }}>
<Canvas camera={{ position: [0, 1, 5], fov: 45 }}>
<Model />
</Canvas>
</div>
In DevTools, select the <canvas> element and check its rendered size. If either dimension is 0, the problem is your CSS, not your scene.
Cause 6: nothing is rendering after the model loads
Loading is asynchronous. If you call renderer.render(scene, camera) once at startup and have no loop, that frame was drawn before the model arrived. Either render again in the load callback or use an animation loop:
renderer.setAnimationLoop(() => {
controls.update();
renderer.render(scene, camera);
});
With react-three-fiber’s frameloop="demand", call invalidate() whenever something changes outside React state.
Cause 7: compressed models without the right decoder
Models optimised with gltf-transform, gltfpack or Blender’s Draco option need a decoder. The loader tells you which one in the console: No DRACOLoader instance provided, setMeshoptDecoder must be called before loading compressed files, or setKTX2Loader must be called before loading KTX2 textures.
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
const draco = new DRACOLoader();
// Copy node_modules/three/examples/jsm/libs/draco/ into public/draco/
draco.setDecoderPath('/draco/');
const loader = new GLTFLoader();
loader.setDRACOLoader(draco);
loader.setMeshoptDecoder(MeshoptDecoder);
In react-three-fiber, drei’s useGLTF sets up Draco and Meshopt for you by default.
Less obvious causes experienced developers miss
- Transparent canvas confusion. With
new THREE.WebGLRenderer({ alpha: true })the page background shows through. A white model on a white page looks like “nothing rendered”. Setscene.backgroundto a colour while you debug. - Inverted normals. A negative scale or a bad export flips the faces, so back-face culling hides the model from the front. Set
material.side = THREE.DoubleSideas a test. If the model appears, fix the export. - Frustum culling on skinned meshes. If an animated character disappears at certain angles, its bounding sphere doesn’t match the posed mesh. Set
mesh.frustumCulled = falseon the skinned meshes. - Material opacity. Some exporters write
opacity: 0or an alpha mode that hides the mesh. Logmaterial.opacityandmaterial.transparent.
Step-by-step diagnosis
- Drop the file into the three.js editor. If it doesn’t show there, the file is the problem, not your code.
- Add
scene.add(new THREE.AxesHelper(5)). If you can’t see the axes, the camera, canvas or render loop is broken. - Add
scene.add(new THREE.BoxHelper(model, 0xff0000))to see where the model actually is and how big it is. - Log
renderer.info.render.callsandrenderer.info.render.trianglesafter a frame. Draw calls above zero mean the GPU is drawing something, so the problem is lighting, colour or position. - Temporarily swap every material for
new THREE.MeshNormalMaterial(). It needs no lights, so if the model appears, the problem is your lighting.
model.traverse((child) => {
if (child.isMesh) child.material = new THREE.MeshNormalMaterial();
});
renderer.render(scene, camera);
console.log(renderer.info.render);
Prevention
- Agree on units (metres) and an origin with whoever exports the models, and check new files in a viewer before they reach the app.
- Always frame the camera from the bounding box rather than from hard-coded positions.
- Ship a default environment map and use direct lights only for emphasis.
- Add an error callback and a visible loading or error state to every loader.
- Optimise models with a consistent pipeline so you always know which decoders you need. My Three.js performance guide covers this.
Key takeaways
- Check the console first. Loading errors explain most blank canvases.
- Centre the model with
Box3and fit the camera to its size. - Black model = no light or environment map for a PBR material.
- Blank canvas in React = parent element with no height.
- Use
AxesHelper,BoxHelper,renderer.infoandMeshNormalMaterialto narrow it down quickly.
Frequently asked questions
Why is my GLTF model black in Three.js?
glTF models use physically based materials, which render black when the scene has no lights and no environment map. Add a RoomEnvironment through PMREMGenerator as scene.environment, plus a directional or hemisphere light.
Why does my Three.js canvas show nothing in React?
The react-three-fiber Canvas fills its parent, so a parent with no height produces a 0-pixel canvas. Give the wrapper an explicit height, such as 100vh or a fixed pixel value.
How do I make a model fit the camera in Three.js?
Compute a Box3 from the model, move the model by the negative of the box centre, then set the camera distance from the largest dimension and the field of view. Update near and far to match and call updateProjectionMatrix().
What does “No DRACOLoader instance provided” mean?
The model’s geometry is Draco-compressed, and GLTFLoader needs a DRACOLoader with a decoder path to unpack it. Create one, point setDecoderPath() at the decoder files and pass it to loader.setDRACOLoader().
Still stuck on a model that won’t render, or building a 3D product viewer from scratch? I build Three.js and react-three-fiber configurators for a living. Get in touch and I can take a look. For a full build walkthrough, see how to build a 3D product configurator.