Glass Caustics (WebGL)
Swimming-pool caustics computed in real time by a WebGL shader — five iterations per pixel, doubled sharpness on Retina screens — seen through glass blurred at 7 px and saturated at 160%; a gradient fallback when WebGL is missing, a still frame when the user reduces motion.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Glassmorphism has 47 effects, including 6 free. Explore the category →
3 usage examples



How it works
A 16/10 scene (.gc-scene, 14 px corners, #04101c background) with two layers: a full-frame <canvas class="gc-canvas">, and the glass .gc-glass — min(72%, 330px) wide, 22 px corners, a white gradient from 16% to 5%, a rim at 28%, backdrop-filter: blur(var(--gc-blur, 7px)) saturate(160%), centered text with drop shadows. The 7 px blur is deliberately light: the light lines remain guessable under the glass, as through thick glass.
The script creates one WebGL context per canvas, compiles a vertex shader that covers the screen with a single triangle, and a fragment shader that computes each pixel: it brings the position into a 2π tile, then runs five iterations where the position is warped by sines and cosines of time; the accumulated intensity, raised to the power 8, gives very thin bright filaments, added to a pool blue (0.04, 0.32, 0.52). Time advances at 0.4 × seconds. The canvas size follows the scene at the screen's density, capped at 2 (measured: a 784 × 490 px canvas for a 784 px scene at density 1), recomputed on every window resize. Each canvas has its own loop: several panes per page work. Measured in Firefox and in Chromium with the graphics card: sharp filaments, continuous rippling.
The script provides three fallbacks. If the user asks for less transparency, or if the browser does not know backdrop-filter, the scene gets .gc-solid; the -webkit- prefix covers Safari 17 and earlier. Without WebGL it gets .gc-nogl: measured by removing WebGL from the browser, the canvas is hidden and a blue radial gradient takes its place. Under reduced motion a single frame is drawn — and redrawn on every resize, since resizing the canvas clears the WebGL buffer: measured in Firefox, the image stays after going from 900 to 700 px wide. An older variant of these caustics also exists in the category.
Accessibility
- Reduced motion: under
prefers-reduced-motion, the script draws a single frame (time 8) and does not start the loop — measured, the image no longer changes. The caustics stay visible, still, even after the window is resized:resize()then redraws the still frame. - Reduced transparency and no blur: the script sets
.gc-solidwhenprefers-reduced-transparency: reducematches or when neitherbackdrop-filternor its prefixed form are known. Measured under emulation in Chromium: solid#08182aglass, caustics lowered to 30%. Firefox 153 and WebKit 26 do not know this transparency setting yet. - Contrast: this is the weak point. The filaments are nearly white and pass under the text. Without the code's drop shadows, measured on the glass at six moments: 3.54:1 median, 1.31:1 when a filament crosses. The shadows (black
text-shadowat 45 and 60%) rescue a short heading; for long or small text, raise the glass veil (first gradient stop at 30% black) or increase--gc-blurto 14 px to drown the filaments. - Semantics: the canvas is decorative — add
aria-hidden="true". Full-frame rippling light can bother motion-sensitive people even when slow: honoringprefers-reduced-motionis already in the script.
Browser compatibility
WebGL 1 + CSS (backdrop-filter) + 3.3 KB of JavaScript, no dependency. WebGL 1 exists in every current browser, including Safari 8 and later. Measured in Firefox 153 and in Chromium 151 with the graphics card (Metal): full caustics. Machines without acceleration get software rendering; measured by forcing Chromium's old software engine in a capture tool, the canvas sometimes came out white or empty from one take to the next — that is the test engine, not a buyer's browser, but it is why the fallback matters. The shader asks for high precision (highp): a device without it fails compilation, and the script switches to the gradient. The glass blurs in all three engines (real screen capture for Firefox and WebKit, whose automated captures never show backdrop-filter).
Without WebGL or if the shader does not compile, the scene gets .gc-nogl: canvas hidden, a radial gradient from #0a6f8c blue to midnight blue, glass and text intact. The second layer of that background, conic-gradient rays, is declared under the opaque radial gradient and never shows: put it first if you want the rays. Without backdrop-filter, .gc-solid makes the glass solid. Without JavaScript, the canvas stays empty on the #04101c background.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="demo-preview">
<div class="gc-scene">
<canvas class="gc-canvas" data-caustics></canvas>
<div class="gc-glass">
<h3>…</h3>
<p>…</p>
</div>
</div>
</div>
The full HTML + CSS + JS of Glass Caustics (WebGL), ready to paste — plus all 840 effects. Or $299 once, lifetime access.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Speed (JS — var speed = 1) |
1 (the shader advances at 0.4 × seconds) | 0.5 for calm pool water, 2 for choppy water. The still frame's time under reduced motion is 8 (render(8.0)). |
Water color (shader — vec3(0.04, 0.32, 0.52)) |
pool blue | Red, green, blue from 0 to 1: vec3(0.02, 0.35, 0.30) for a green lagoon, vec3(0.02, 0.08, 0.20) for night water. |
Filament fineness (shader — pow(abs(c), 8.0)) |
8 | 4 gives wide sheets of light, 12 very thin, rarer threads. |
Cell size (shader — uv * 6.28318) |
one caustic tile per scene, stretched to the scene's ratio | Multiply by 2 (uv * 12.566) for cells half the size. For round cells whatever the ratio, divide by the height only: gl_FragCoord.xy / u_res.y. |
Glass blur (CSS — --gc-blur) |
7px | 14 px drowns the filaments behind the text (better contrast), 3 px leaves them almost sharp. |
Fallback background (CSS — .gc-scene.gc-nogl) |
blue radial gradient (conic rays hidden underneath) | Use a pool image as url(…) center / cover: that is what devices without WebGL will see. |
Sharpness (JS — Math.min(window.devicePixelRatio || 1, 2)) |
screen density, capped at 2 | Cap at 1 on a large scene to divide the number of computed pixels by four on Retina screens; the glass blur hides the loss of sharpness. |
FAQ
IntersectionObserver when the scene leaves the screen.getContext('webgl') then experimental-webgl; if neither answers, or a shader does not compile, or the program does not link, it sets .gc-nogl on the scene and stops. Measured by removing WebGL from the browser: canvas hidden, blue radial gradient instead, glass and text intact, no error. A context lost along the way (tab asleep for long on mobile) is not handled: the canvas stays frozen on its last frame..gc-canvas and creates one WebGL context per canvas, each with its own loop. Browsers limit the number of active WebGL contexts (around sixteen): beyond that, the oldest are lost. For a grid of cards, prefer a single canvas behind the whole grid.