threlte logo
@threlte/extras

<ShadowAlpha>

webgl <ShadowAlpha> works with WebGL only.

This component is a port of drei’s <ShadowAlpha> component.

By default in Three.js, shadows are always fully opaque regardless of the material’s opacity or alpha map. <ShadowAlpha> fixes this by making a mesh’s shadow respect its material’s opacity and alphaMap.

Place it as a child of the mesh whose shadow you want to control:

<T.Mesh castShadow>
  <T.BoxGeometry />
  <T.MeshStandardMaterial
    transparent
    opacity={0.5}
  />
  <ShadowAlpha />
</T.Mesh>

The shadow will now be 50% transparent, matching the material’s opacity.

Alpha Maps

If the parent material has an alphaMap (e.g. a leaf texture with cutouts), the shadow will automatically match those cutouts:

<T.Mesh castShadow>
  <T.PlaneGeometry />
  <T.MeshStandardMaterial
    transparent
    map={leafTexture}
    alphaMap={leafAlpha}
  />
  <ShadowAlpha />
</T.Mesh>

You can override the alpha map or disable it:

<!-- Use a custom alpha map -->
<ShadowAlpha alphaMap={customTexture} />

<!-- Disable alpha map, only use opacity -->
<ShadowAlpha alphaMap={false} />

How It Works

<ShadowAlpha> uses Bayer dithering in custom depth and distance materials. Fragments below the opacity threshold are discarded from the shadow map, creating a pattern that approximates transparency. This is a screen-space technique, so the dither pattern may be visible at close range.

This component is a workaround for a long-standing Three.js limitation. It will be deprecated when Three.js adds native support for shadows from transparent objects.

WebGPU

There is no WebGPU implementation it isn’t needed. Node materials expose castShadowNode, which does the same job in a single prop and gives a true fractional shadow rather than a dither pattern:

Scene.svelte
<script lang="ts">
  import { T, useThrelte } from '@threlte/core/webgpu'
  import { uniform, vec4 } from 'three/tsl'

  const { renderer } = useThrelte()

  // Set this before the first render, because compiled shadow shaders are never invalidated.
  renderer.shadowMap.transmitted = true

  const shadowOpacity = uniform(0.5)
</script>

<T.Mesh castShadow>
  <T.TorusKnotGeometry args={[0.6, 0.2, 128, 32]} />
  <T.MeshStandardNodeMaterial
    transparent
    opacity={0.5}
    castShadowNode={vec4(0, 0, 0, shadowOpacity)}
  />
</T.Mesh>

castShadowNode is the color the shadow is drawn with, so vec4(1, 0, 0, shadowOpacity) casts a red one. It’s independent of the material’s own opacity — nothing is inherited, so a fully opaque mesh can cast a half-strength shadow.

<script lang="ts">
  import { Canvas } from '@threlte/core'
  import { Pane, Slider, Checkbox } from 'svelte-tweakpane-ui'
  import Scene from './Scene.svelte'

  let shadowOpacity = $state(0.5)
  let meshOpacity = $state(0.5)
  let overrideOpacity = $state(false)
</script>

<Pane
  title="ShadowAlpha"
  position="fixed"
>
  <Slider
    bind:value={meshOpacity}
    label="material opacity"
    min={0}
    max={1}
    step={0.01}
  />
  <Checkbox
    bind:value={overrideOpacity}
    label="override shadow opacity"
  />
  <Slider
    bind:value={shadowOpacity}
    label="shadow opacity"
    min={0}
    max={1}
    step={0.01}
    disabled={!overrideOpacity}
  />
</Pane>

<div>
  <Canvas>
    <Scene
      {meshOpacity}
      shadowOpacity={overrideOpacity ? shadowOpacity : undefined}
    />
  </Canvas>
</div>

<style>
  div {
    height: 100%;
  }
</style>
<script lang="ts">
  import { T } from '@threlte/core'
  import { Float, OrbitControls, ShadowAlpha } from '@threlte/extras'

  interface Props {
    meshOpacity: number
    shadowOpacity: number | undefined
  }

  let { meshOpacity, shadowOpacity }: Props = $props()
</script>

<T.PerspectiveCamera
  makeDefault
  position={[4, 4, 4]}
  fov={35}
>
  <OrbitControls
    autoRotate
    autoRotateSpeed={0.5}
    enableDamping
    target.y={0.8}
  />
</T.PerspectiveCamera>

<T.DirectionalLight
  castShadow
  intensity={2}
  position={[3, 6, 3]}
  shadow.mapSize.width={1024}
  shadow.mapSize.height={1024}
  shadow.camera.left={-4}
  shadow.camera.right={4}
  shadow.camera.top={4}
  shadow.camera.bottom={-4}
/>
<T.AmbientLight intensity={0.4} />

<!-- Ground -->
<T.Mesh
  receiveShadow
  rotation.x={-Math.PI / 2}
>
  <T.PlaneGeometry args={[10, 10]} />
  <T.MeshStandardMaterial color="#f0ebe3" />
</T.Mesh>

<!-- Floating torus knot -->
<Float
  floatIntensity={0.5}
  floatingRange={[0, 0.3]}
>
  <T.Mesh
    castShadow
    position.y={1.2}
    rotation={[0.4, 0.6, 0]}
  >
    <T.TorusKnotGeometry args={[0.6, 0.2, 128, 32]} />
    <T.MeshStandardMaterial
      color="#6c5ce7"
      transparent
      opacity={meshOpacity}
    />
    <ShadowAlpha opacity={shadowOpacity} />
  </T.Mesh>
</Float>

Component Signature

Props

name
type
required
description

alphaMap
THREE.Texture | false
no
Alpha map for the shadow. Defaults to parent material alphaMap. Pass false to disable.

opacity
number
no
Shadow opacity. Defaults to parent material opacity.