šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
HTML MASTER CLASS /// LEARN TAGS /// BUILD STRUCTURE /// SEMANTIC WEB /// HTML MASTER CLASS /// LEARN TAGS ///

Shadows in Three.js 3D WebGL

Understand the 4-step process to enable shadows in Three.js and how to optimize shadow map resolution.

⚔ Total XP: 0|šŸ’» threejs XP: 0

Skill Matrix

UNLOCK NODES BY LEARNING NEW TAGS.

System Hub

Core shadow concepts.

Quick Quiz //

How many separate places must you enable shadows for them to appear?


šŸš€ LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
šŸŽ“ COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.

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 shadows
localhost:3000
Browser Preview
WebGL Output
3D 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 shadow
localhost:3000
Browser Preview
WebGL Output
3D 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;
localhost:3000
Browser Preview
WebGL Output
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>
localhost:3000
Browser Preview
WebGL Output
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>
localhost:3000
Browser Preview
WebGL Output
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 />
localhost:3000
Browser Preview
WebGL Output
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>
localhost:3000
Browser Preview
WebGL Output
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>
localhost:3000
Browser Preview
WebGL Output
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 />);
localhost:3000
Browser Preview
WebGL Output
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;
localhost:3000
Browser Preview
WebGL Output
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!
localhost:3000
Browser Preview
WebGL Output
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

ChromeSupported

Fully supported.

FirefoxSupported

Fully supported.

SafariSupported

Fully supported.

EdgeSupported

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

THE BUG

Enabling shadows on the Canvas and setting castShadow/receiveShadow correctly, but still seeing no shadow because the light itself is missing castShadow.

THE FIX

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>

Interview Prep

?Frequently Asked Questions

Pascual Vila

Pascual Vila

Frontend Instructor // Code Syllabus

Common Pitfalls & Errors

The Error //

Not reading error messages carefully

Uncaught TypeError: Cannot read properties of undefined (reading 'length') // Solution: Ensure the variable you are calling .length on is initialized as a string or an array, not undefined.

The Solution //

Most of the time, the compiler or interpreter tells you exactly what line caused the crash and why. Read stack traces from the top down to identify the root cause.

The Error //

Hardcoding sensitive credentials

// Wrong const API_KEY = 'sk-123456789'; // Correct const API_KEY = process.env.API_KEY;

The Solution //

Never hardcode API keys, passwords, or secrets in your source code. Use environment variables (.env files) to keep them secure and out of version control.

Continue Learning