Listen up. If you're building modern applications, understanding Shadows in Three.js 3D WebGL is non-negotiable. This is where simple logic turns into intelligent behavior.
1Why Shadows Matter for Realism
Lights are great, but without shadows, objects look like they are floating in space. Shadows ground your objects and add immense realism ā they're the visual cue that tells a viewer's brain where an object actually sits relative to its surroundings.
Unlike lighting, which Three.js calculates constantly as part of normal rendering, shadows require an entirely separate rendering pass (a 'shadow map') from each shadow-casting light's perspective, which is exactly why they're opt-in rather than automatic.
// š¦ Enter the shadows3D Scene rendered. Objects: 4, Draw Calls: Optimized.
2The Four-Step Shadow Checklist
Shadows in Three.js are expensive to compute. Therefore, they are turned OFF by default everywhere. You must explicitly enable them in 4 different places ā the renderer, the light, the object casting the shadow, and the object receiving it.
Missing any single one of these four steps results in shadows silently not appearing at all, with no error or warning ā this makes 'no shadows showing up' one of the most common Three.js debugging sessions, usually solved by checking this exact checklist in order.
// 1. Renderer
// 2. Light
// 3. Object casting shadow
// 4. Object receiving shadow3D Scene rendered. Objects: 4, Draw Calls: Optimized.
3Step 1: Enabling the Renderer's Shadow Map
Step 1: You must tell the Renderer that it is allowed to calculate shadows, via renderer.shadowMap.enabled = true. Without this, none of the other three shadow-related settings have any effect at all.
The shadowMap.type property also matters: THREE.PCFSoftShadowMap produces softer, more natural-looking shadow edges compared to the default THREE.PCFShadowMap, at a small additional rendering cost ā a worthwhile tradeoff for most production scenes.
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;3D Scene rendered. Objects: 4, Draw Calls: Optimized.
4Step 1 in R3F: The shadows Prop
In React Three Fiber, you simply add the shadows prop to the Canvas component to enable shadows on the renderer ā this single boolean prop replaces both the shadowMap.enabled and shadowMap.type lines needed in vanilla Three.js.
R3F defaults shadows to PCFSoftShadowMap when this prop is set, matching the soft-shadow best practice covered in the previous section, without requiring you to configure the shadow map type explicitly.
<Canvas shadows>
{/* Scene */}
</Canvas>3D Scene rendered. Objects: 4, Draw Calls: Optimized.
5Distinguishing shadows from castShadow
The prop you add to the Canvas in React Three Fiber to enable the shadow map on the renderer is simply shadows ā a common early mistake is confusing this renderer-level prop with castShadow or receiveShadow, which are separate, per-object props covered in the next steps.
Remembering the distinction helps avoid confusion: shadows on Canvas turns the capability ON globally, while castShadow and receiveShadow decide which specific objects participate once that capability exists.
<Canvas ???>
<mesh />
</Canvas>3D Scene rendered. Objects: 4, Draw Calls: Optimized.
6Step 2: Enabling Shadows on a Light
Step 2: Tell a specific Light that it should generate shadows. Not all lights can cast shadows (e.g., AmbientLight cannot) ā AmbientLight has no direction or position, so there's no meaningful shadow geometry to calculate from it.
DirectionalLight and SpotLight are the most common shadow-casting light types, since both have a clear direction that the renderer can use to compute a shadow map from the light's point of view.
directionalLight.castShadow = true;
// R3F:
<directionalLight castShadow />3D Scene rendered. Objects: 4, Draw Calls: Optimized.
7Steps 3 & 4: castShadow and receiveShadow
Step 3 & 4: Tell the objects what to do. The object blocking the light must castShadow. The object behind it must receiveShadow ā these are the final two switches, applied per-mesh rather than globally.
A single mesh can have both props set at once (an object can both cast a shadow onto the floor and receive a shadow from another object above it), but by default a mesh has neither enabled, so both must be added deliberately wherever shadow interaction is needed.
<mesh castShadow>
<boxGeometry />
</mesh>
<mesh receiveShadow position={[0, -1, 0]}>
<planeGeometry />
</mesh>3D Scene rendered. Objects: 4, Draw Calls: Optimized.
8Applying the Checklist to a Cube on a Floor
If you have a cube sitting on a floor, the floor's setting should be receiveShadow so the cube's shadow appears on it ā the floor isn't blocking any light itself, it's the surface the shadow gets projected onto.
The cube, in this same scenario, needs castShadow instead ā mixing these two up (giving the floor castShadow and the cube receiveShadow) is a common mistake that results in no visible shadow at all, since neither object is doing the job the scene actually needs.
<mesh ???>
<planeGeometry /> {/* The Floor */}
</mesh>3D Scene rendered. Objects: 4, Draw Calls: Optimized.
9Putting All Four Steps Together
Let's put it all together! Here is a sphere casting a shadow onto a plane ā this example demonstrates all four checklist items at once: shadows on Canvas, castShadow on the directionalLight, castShadow on the sphere, and receiveShadow on the floor plane.
Notice the floor is rotated with rotation={[-Math.PI / 2, 0, 0]} to lie flat ā PlaneGeometry is vertical by default, so this rotation is what turns it into a horizontal ground surface capable of visibly catching the sphere's shadow.
const App = () => {
return (
<Canvas shadows camera={{ position: [0, 2, 5] }}>
<ambientLight intensity={0.5} />
{/* Light casting shadow */}
<directionalLight
castShadow
position={[2.5, 5, 2.5]}
intensity={2}
shadow-mapSize={[1024, 1024]}
/>
{/* Object casting shadow */}
<mesh castShadow position={[0, 1, 0]}>
<sphereGeometry args={[0.8, 32, 32]} />
<meshStandardMaterial color="hotpink" />
</mesh>
{/* Floor receiving shadow */}
<mesh receiveShadow rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[10, 10]} />
<meshStandardMaterial color="white" />
</mesh>
</Canvas>
);
};
render(<App />);3D Scene rendered. Objects: 4, Draw Calls: Optimized.
10Fixing Pixelated Shadow Edges
Shadows look pixelated by default. To fix this, you must increase the light's shadow map resolution. 1024x1024 or 2048x2048 are common values ā the shadow map is essentially a texture rendered from the light's perspective, and low resolution means visibly blocky, jagged shadow edges.
Higher resolution shadow maps cost more GPU memory and rendering time, so this is another tunable tradeoff: use the lowest resolution that still looks acceptable for your specific scene and camera distance, rather than defaulting to the maximum everywhere.
// R3F
<directionalLight shadow-mapSize={[1024, 1024]} />
// Vanilla
light.shadow.mapSize.width = 1024;
light.shadow.mapSize.height = 1024;3D Scene rendered. Objects: 4, Draw Calls: Optimized.
11Wrapping Up: Shadows in Moderation
Awesome! Your scenes now have depth and grounding. Remember, shadows are performance-heavy, so use them sparingly ā not every light or object needs to participate in shadow casting.
A common optimization pattern is enabling castShadow/receiveShadow only on the handful of objects where shadows are visually important (like a character and the ground beneath them), while leaving background or decorative objects without shadow props entirely to keep the shadow map computation cheap.
// š Shadows rendered!3D Scene rendered. Objects: 4, Draw Calls: Optimized.
12Step-by-Step Breakdown
Lights are great, but without shadows, objects look like they are floating in space. Shadows ground your objects and add immense realism.
Shadows in Three.js are expensive to compute. Therefore, they are turned OFF by default everywhere. You must explicitly enable them in 4 different places.
Step 1: You must tell the Renderer that it is allowed to calculate shadows.
In React Three Fiber, you simply add the shadows prop to the <Canvas> component to enable shadows on the renderer.
What prop do you add to the <Canvas> in React Three Fiber to enable the shadow map on the renderer?
- āshadowMap
- ācastShadow
- āshadows
Step 2: Tell a specific Light that it should generate shadows. Not all lights can cast shadows (e.g., AmbientLight cannot).
Step 3 & 4: Tell the objects what to do. The object blocking the light must castShadow. The object behind it must receiveShadow.
If you have a cube sitting on a floor, what should the floor's setting be so the cube's shadow appears on it?
- ācastShadow
- āreceiveShadow
- āshadows
Let's put it all together! Here is a sphere casting a shadow onto a plane. Watch the live 3D preview.
Shadows look pixelated by default. To fix this, you must increase the light's shadow map resolution. 1024x1024 or 2048x2048 are common values.
Which property on the light determines the resolution (and thus the crispness/quality) of the shadow it casts?
- āresolution
- āmapSize
- āquality
Awesome! Your scenes now have depth and grounding. Remember, shadows are performance-heavy, so use them sparingly.
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Shadows Are a Visual-Only Depth Cue ā Don't Rely on Them Alone
A shadow indicating an object 'floats' above a surface versus 'rests' on it is imperceptible without sight ā if that distinction is functionally important (like a drag-and-drop drop target), pair it with an explicit ARIA state rather than relying on the shadow alone.
<div aria-dropeffect={isHovering ? 'move' : 'none'}>...</div>SEO Implications
- 1
Shadow Map Configuration Is Pure Runtime GPU State
Shadow map resolution and casting/receiving flags exist only as WebGL render targets during execution ā none of it is crawlable, so this page's search value comes from explaining the four-step enablement checklist in text, not from any rendered shadow itself.
Best Practices
Only Enable castShadow/receiveShadow on Objects Where Shadows Are Visually Important
Shadow computation cost scales with how many objects participate ā enabling castShadow on every mesh in a scene 'just in case' wastes performance on shadows nobody will notice. Reserve it for foreground objects and their immediate ground plane.
Match Shadow Map Resolution to the Scene's Actual Scale and Camera Distance
A tiny scene viewed up close needs less shadow map resolution than a sprawling outdoor scene to look equally crisp. Test at the resolution your scene is actually viewed at rather than defaulting to the highest setting everywhere.
Frequent Bugs
Enabling shadows on the Canvas and setting castShadow/receiveShadow correctly, but still seeing no shadow because the light itself is missing castShadow.
All four steps ā renderer/Canvas shadows, light castShadow, object castShadow, and object receiveShadow ā must be set for a shadow to appear. Missing any single one silently produces no shadow with no error, so when debugging, check all four in order rather than assuming the object props alone are enough.
Real-World Examples
Grounding a Product in a 3D Configurator
Interactive product viewers (furniture, sneakers, cars) commonly render just one shadow-casting directionalLight above the product and one receiveShadow-enabled invisible ground plane beneath it, giving the illusion of a photography studio floor without needing a full lighting rig.
<directionalLight castShadow position={[5, 8, 5]} shadow-mapSize={[2048, 2048]} />
<mesh receiveShadow rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[20, 20]} />
<shadowMaterial opacity={0.3} />
</mesh>