Glassmorphism✨ Premium

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.

WebGLShaderCaustics

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

A natural swimming pool's project sheet — a pool builder's website — Glass Caustics (WebGL) example 1

① A natural swimming pool's project sheet — a pool builder's website

WhenA project page: water reflections move under the spec sheet (size, depth, plant filtering, chlorine-free water), between the stone coping and the reeds.
WhyThe caustics show how clear the water is, the main argument for a natural pool; the shader stays smooth even full frame, where a video would weigh several megabytes.
Settings258 px glass top left, left-aligned text, spec sheet as a two-column <dl>; coping as a 16 px jointed border, reeds as oblique gradients placed between the canvas and the glass.
A diving center's home page — a club in Brittany — Glass Caustics (WebGL) example 2

② A diving center's home page — a club in Brittany

WhenThe club's home page: underwater light ripples behind the list of next outings, with level, depth and places left.
WhyCaustics are what a diver sees looking at the sea floor: the mood is immediate, with no photo to load, and the glass keeps the dates readable.
Settings250 px glass over the full height on the right; dates as dark pills; a downward gradient veil above the canvas to ground the club name and the “Try a first dive” button.
Label for the jellyfish tank — a public aquarium's screen — Glass Caustics (WebGL) example 3

③ Label for the jellyfish tank — a public aquarium's screen

WhenA screen next to the tank: species name, Latin name, size, lifespan and habitat, over water light where jellyfish float.
WhyThe water light on the screen extends the real tank; and on an old kiosk without WebGL, the gradient fallback guarantees a display instead of a black screen.
Settings300 px label bottom left; translucent CSS jellyfish (bell, gonads, tentacles masked into a fade) rising and sinking over 5 s, stopped under reduced motion; a radial veil darkening the edges.

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-solid when prefers-reduced-transparency: reduce matches or when neither backdrop-filter nor its prefixed form are known. Measured under emulation in Chromium: solid #08182a glass, 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-shadow at 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-blur to 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: honoring prefers-reduced-motion is 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).

Chrome 76+✓ Full
Firefox 103+✓ Full
Safari 13+✓ Full (-webkit- prefix)
Edge 79+✓ Full
Mobile iOS 13+✓ Full
Android Chrome 76+✓ Full (gradient on devices without high precision)

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

index.html — structure
<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>
Unlock the full code — $9.90/mo incl. tax

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 / propertyDefaultEffect
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

It computes every pixel every frame: on a 1,280 × 800 px scene at density 2, that is 4 million pixels, five iterations each, sixty times a second. A recent graphics card does it easily; an old laptop warms up. The loop also runs when the scene is off screen. Two levers: cap the density at 1, and pause the loop with an IntersectionObserver when the scene leaves the screen.
The script tries 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.
Yes: the script walks through every .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.