Navigate
Search topics across all sections
Shadows
Shadows are what make your 3D objects feel like they actually exist in a space. Without them, things look like they are floating. Getting shadows working takes three specific steps — miss any one and you get nothing.
You set up a scene with a directional light and a box on a ground plane. You expect to see a shadow beneath the box, but there is nothing — no shadow at all. You increase the light intensity, change materials, reposition the camera — still nothing. The problem? Shadows are disabled by default and you need to enable them in three separate places.
No shadows visible despite having lights and meshes in the scene.
Real-world
Think of shadows like a puppet shadow play. You need exactly three things for the show to work.
A lamp (the light source) — without a lamp, there is no light to cast shadows. In Three.js, the light needs castShadow turned on.
A puppet (the shadow caster) — this is the object that blocks the light. Your mesh needs castShadow turned on.
A wall (the shadow receiver) — this is the surface where the shadow appears. Your ground plane needs receiveShadow turned on.
Take away any one of these three, and the shadow show does not happen. That is exactly how Three.js shadows work — you must enable all three or you get nothing.
The Three Things You Need
Shadows are off by default for performance. You must enable them at three levels. Miss any one and you get zero shadows.
1. Canvas
<Canvas shadows>
2. Light
castShadow on the light
3. Meshes
castShadow + receiveShadow
Shadows appear!
All three enabled
Guided Walkthrough
Let us get shadows working from scratch in three steps.
Step 1 — Enable shadows on the Canvas and light
<Canvas shadows>
<directionalLight
position={[5, 10, 5]}
castShadow
/>
</Canvas>The shadows prop on Canvas tells the renderer to enable shadow mapping. The castShadow prop on the light tells it to actually generate a shadow map. Without both, nothing happens.
Step 2 — Mark which meshes cast and receive shadows
{/* Box casts and receives shadows */}
<mesh castShadow receiveShadow>
<boxGeometry />
<meshStandardMaterial />
</mesh>
{/* Ground only receives shadows */}
<mesh receiveShadow rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[20, 20]} />
<meshStandardMaterial />
</mesh>The box gets both castShadow (it blocks light) and receiveShadow (other shadows can fall on it). The ground only needs receiveShadow because it is the surface where shadows appear.
Step 3 — Tune shadow quality with camera and map size
<directionalLight castShadow
shadow-mapSize-width={2048}
shadow-mapSize-height={2048}
shadow-camera-left={-5}
shadow-camera-right={5}
shadow-camera-top={5}
shadow-camera-bottom={-5}
shadow-bias={-0.0005}
/>The shadow camera bounds control how much area the shadow map covers. Tighter bounds mean sharper shadows. The bias prevents dark stripe artifacts. Start small and increase until stripes disappear.
<Canvas>
<directionalLight position={[5, 10, 5]} />
<mesh>
<boxGeometry />
<meshStandardMaterial />
</mesh>
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry args={[20, 20]} />
<meshStandardMaterial />
</mesh>
</Canvas>Without shadows, the box looks like it's floating — there's no visual connection to the ground.
What you just learned
Shadows need three things enabled: Canvas, light (castShadow), and meshes (castShadow/receiveShadow)
The shadow camera frustum controls quality — tighter bounds mean sharper shadows
shadow.bias prevents shadow acne (dark stripe artifacts from self-shadowing)
Only enable castShadow on objects that meaningfully contribute to the shadow — skip tiny decorations
Question
If increasing the shadow map size from 1024 to 4096 gives you four times sharper shadows, why not always use the biggest map possible? Think about what that means for GPU memory and rendering time, especially on mobile devices.
Think about it...
You have a scene with shadows enabled on the Canvas and a directional light with castShadow. A box has castShadow=true but the ground plane has receiveShadow=false. What will you see?
Try These Challenges
Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.
Try This!
Beginner
Switch shadow type to Basic — see the hard edges
Try This!
Intermediate
Set bias to 0.01 — shadows detach!
Try This!
Beginner
Lower shadow map to 256 — pixel shadows
These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.
// Only enabled on the renderer
<Canvas shadows>
<directionalLight position={[5, 10, 5]} />
{/* Missing: castShadow on the light */}
<mesh>
<boxGeometry />
<meshStandardMaterial />
</mesh>
{/* Missing: castShadow and receiveShadow on meshes */}
</Canvas><directionalLight castShadow
shadow-camera-left={-50}
shadow-camera-right={50}
shadow-camera-top={50}
shadow-camera-bottom={-50}
shadow-mapSize-width={2048}
shadow-mapSize-height={2048}
/>
// 2048 pixels spread over 100 units = terrible quality<directionalLight castShadow
shadow-mapSize-width={2048}
shadow-mapSize-height={2048}
/>
// No bias — surfaces incorrectly shadow themselves
// creating dark striped patternsBest Practices
Use CameraHelper during setup
Add a CameraHelper for your shadow camera to see exactly what area the shadow map covers. This makes it easy to tighten the frustum for sharper shadows.
Default to PCFSoftShadowMap
PCFSoftShadowMap gives the best quality-to-performance ratio for most scenes. Only switch to BasicShadowMap if shadow filtering is a measured bottleneck.
Be selective with castShadow
Every mesh with castShadow adds to the shadow render pass. Skip tiny decorations, distant objects, and transparent meshes to keep performance high.
Consider ContactShadows for grounding
If you only need a soft shadow beneath objects to keep them from floating, drei's ContactShadows component is cheaper and easier than full shadow mapping.