shadcn.io is not affiliated with official shadcn/ui
Photoelastic Shader draws clear plastic between crossed polarizers, on black. Loads press on it at a few points and drift on slow paths, and round each one smooth bands of color ring outward, in the Michel-Lévy order: pink and green at the heart, then blue, magenta, orange, straw and a pale gray halo, merging into contour lines where two loads come close. Dark brushes sweep through as the stress turns. The colors aren't painted: each pixel finds the stress, turns it into a retardation, and lets ten wavelengths through as sin²(πR/λ), summed into color with Wyman, Sloan and Shirley's fit to the CIE color-matching functions, so the Michel-Lévy sequence appears by itself. A WGSL fragment shader on WebGPU draws it each frame.
Use it behind an engineering, materials-science or physics hero, a lab or a research page. For rainbow arcs from a grooved surface, using the same spectral color, use Diffraction Shader; for free-form colored bands, Plasma Shader. It runs only while it's on screen, holds a still frame with reduced motion, and shows CSS rings of the same colors in browsers without WebGPU.
Installation
Usage
import {
PhotoelasticShader,
} from "@/components/ui/photoelastic-shader";<PhotoelasticShader className="flex min-h-[32rem] items-center justify-center">
<div className="rounded-xl bg-black/60 px-8 py-6">
<h1 className="text-5xl font-semibold text-white">Ship faster</h1>
</div>
</PhotoelasticShader>Photoelastic Shader is a container: your content sits on top of the plastic, in normal flow. It fills its own box, so give it a height or content. For a backdrop behind the whole page, render it empty with className="fixed inset-0 -z-10". The fringes are bright and the gaps black, so no text color reads on it bare: put copy on a panel, like the bg-black/60 one above, which keeps white text above 5.8:1 everywhere.
Demos
Parallel polarizers
A light backgroundColor, here #f7f7f5, turns the polarizers parallel: the plastic is white where there's no stress, and every fringe shows its complementary color, so the pale halos go dark and each band turns to its complement.
In a card
loads={2} keeps two loads drifting, and interactive lets the pointer press a third: its fringes follow the pointer and merge with the others where they meet. When the pointer leaves, its load lets go and its fringes fade away.
Performance
About 0.6 ms of GPU time a frame at 1280×720 on an M2 Max laptop; slower GPUs take several times longer. The fringes tighten toward each load, so keep full quality unless you need the time: at quality={0.5} the finest rings go soft. The animation stops when the shader scrolls out of view, and the browser pauses it in a hidden tab. Every shader on a page shares one GPU device and one frame loop, and vgpu (about 50 KB gzipped) loads only when the first one mounts.
Browser support
The shader needs WebGPU: Chrome and Edge, Safari 26 and later, and Firefox on Windows and macOS. Elsewhere, and until the first frame, the box shows three CSS ring sets in the same colors on your background, so the layout never jumps. data-state on the root reads unsupported there, if you want to style around it. See Shader Canvas.
Accessibility
The plastic is decoration: it's hidden from assistive tech and lets every click through to your content. With reduced motion it draws one still frame, three ringed loads with brushes between them, and stops. Nothing flashes: the loads drift and press over many seconds, so the rings flow slowly in and out. The contrast inside the pattern is high and changes everywhere, so never set text on it bare: a bg-black/60 panel keeps white text above 5.8:1, and bg-black/70 above 8.6:1.
FAQ
Can I change the colors?
The fringe colors come from the optics, so they keep the Michel-Lévy order. tintColor is the light shining through, which colors every fringe, and backgroundColor what's behind the plastic, in any CSS color. A light backgroundColor turns the polarizers parallel and the fringes complementary. intensity sets the saturation, from 0 (gray fringes) to 2.
How do I get more or fewer rings?
stress sets how hard the loads press, from 0.2 to 3. At 1 each load reaches about four fringe orders at its heart; at 3 the rings crowd in and the highest orders wash out to a pale gray, as they do in real plastic. loads sets how many points press, from 1 to 5.
What are the dark brushes?
They're isoclinics: where the stress runs at 45 degrees to the polarizers, less light gets through. isoclinics sets how dark they are, from 0 (none) to 1 (black).
Can visitors press on it?
Yes: with interactive, the pointer presses one more load, which follows it with a short ease, fades in as the pointer comes over the plastic and lets go when it leaves. Touch is ignored, so scrolling on a phone never moves it. Without interactive, there's no extra load at all.
API Reference
PhotoelasticShader
data-slot="photoelastic-shader"
backgroundColor?string
Behind the plastic. A light one turns the polarisers parallel: white where there's no stress, and the complementary colours. Any CSS colour. @default "#050507".
- Default
"#050507"
fallback?string
What shows until the first frame and in browsers without WebGPU: a CSS
background value (gradients, sizes, a colour) that looks like the
shader. It can use the colour props' --shader-<field> variables.
intensity?number
Colour saturation, from 0 (grey fringes) to 2. @default 1.
- Default
1
interactive?boolean
Whether u.pointer follows the pointer. @default false.
- Default
false
isoclinics?number
How strongly the dark brushes cross the fringes, from 0 (none) to 1. @default 0.35.
- Default
0.35
loads?number
How many loads press on the plastic, from 1 to 5. @default 3.
- Default
3
paused?boolean
Holds the current frame. @default false.
quality?number
Render scale, from 0.25 to 1, of the screen's pixels (capped at 2×). @default 1.
speed?number
Animation speed: 1 is normal, 0 holds the frame. @default 1.
stress?number
How hard the loads press, from 0.2 to 3: more stress, more fringe orders round each. @default 1.
- Default
1
time?number
A fixed moment, in seconds: draws it and doesn't animate.
tintColor?string
The light shining through, which colours every fringe. Any CSS colour. @default "#ffffff".
- Default
"#ffffff"
render?ReactElement | ((props: object, state: object) => ReactElement)
Replace the host element or compose it with another component.
- Type
ReactElement | ((props: object, state: object) => ReactElement)
className?string
Forwarded to the part.
Related
- Pro


Diffraction Shader
Light & glassRainbow arcs from a grooved surface
- Pro


Oil Slick Shader
Flow & gradientsThin-film color swirling on asphalt


Plasma Shader
Flow & gradientsFree-form colored bands, free and lighter


Streamlines Shader
Flow & gradientsFlow round moving shapes, as hairlines, free
- Pro


Smooth Voronoi Contours Shader
Patterns & tilingsBold contour rings, poster-style
- Pro


Quasicrystal Shader
GeometryBanded order for a science page