@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:
<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>