Liquid Glass

Which renderer, and how to implement it

There are two renderers behind one API. You pick by answering a single question: is the thing behind the glass available as a texture, or is it the live page? Each recipe below is live in this browser, with the exact code and the file that implements it.

in your browser, glassify() resolves to:

The decision

Behind the glass is…UseRefractionImplemented in
an image / video / canvas you can supply new LiquidGlass({ background }) · WebGL full — every browser (Firefox too) glass-pool.js, shaders.js
the live page (arbitrary scrolling DOM) glassify(selector) · DOM Chromium · blur on Firefox / Safari dom-renderer.js

Why not “full refraction of any live element, everywhere”? That’s the iOS Liquid Glass trick, and it needs the OS compositor to sample the real backdrop in real time. The web has no such API — backdrop-filter is the closest, and only Chromium resolves the SVG displacement it needs. So for cross-browser refraction you give the glass a texture (WebGL); for adopting arbitrary live UI you use glassify() and accept blur off-Chromium.

What each tier can render

The library picks a tier at runtime and exposes it as glass.tier. Read it to degrade gracefully — e.g. lean on tint + edge highlight when refraction isn't available.

tierblurtint · bevel · sheenrefractionchromatic aberrationyou get it when…
webglyou pass a background texture
displacementglassify() on Chromium
blurglassify() on Firefox / Safari
flatgradients onlyno backdrop-filter at all
1

WebGL over an image — full refraction, every browser

implemented in src/glass-pool.js + src/shaders.js

You supply the backdrop as a texture, so a GPU shader does the refraction. This is the path with full refraction on Firefox and Safari — it never touches backdrop-filter. Use it for hero art, media tiles, players over a poster, anything with a known background.

import { LiquidGlass } from 'liquid-glass';

// stage is position:relative and the image covers it
const glass = new LiquidGlass({
  stage,
  background: imageEl,      // img / canvas / video
  renderer: 'webgl',
  width: 220, height: 120, x: 40, y: 55,
  refraction: 0.8, chromAberration: 0.1, blurAmount: 0.08,
});
stage.appendChild(glass.element);
glass.content.textContent = 'Play';
Chromium ✓ refractionFirefox ✓ refractionSafari ✓ refraction
2

WebGL over live video / canvas — full refraction, every browser

implemented in src/glass-pool.js (immutable texture upload)

Same WebGL path, but the texture updates every frame. Pass a <video> or an animated canvas with dynamicCanvas: true. The frame is uploaded in place (texSubImage2D) so it never reallocates GPU memory. This is how you glass over playing video, everywhere.

const glass = new LiquidGlass({
  stage,
  background: videoEl,        // or an animated canvas…
  dynamicCanvas: true,      // …with this flag
  renderer: 'webgl',
  refraction: 0.7, distortion: 0.15,
});
Chromium Firefox Safari
3

glassify() the live page — adopt any element in place

implemented in src/dom-renderer.js
Live glass
↑ the tiles behind are real DOM — the card refracts them

No texture. You hand glassify() a selector and it turns that element into glass in place — keeping its children, layout and click handlers — refracting whatever is actually behind it on the page. Full refraction on Chromium; frosted blur on Firefox / Safari (they can’t resolve SVG displacement in backdrop-filter).

import { glassify } from 'liquid-glass';

// #demo-card is ordinary styled HTML already on the page
glassify('#demo-card', {
  refraction: 0.7, blurAmount: 0.14,
  edgeHighlight: 0.5, chromAberration: 0.08,
});
// children stay clickable; its opaque background is cleared
Chromium ✓ refractionFirefox blurSafari blur
4

Same thing from a web component or React

implemented in src/web-component.js · src/react.js
<!-- Web component -->
import { defineLiquidGlass } from 'liquid-glass/web-component';
defineLiquidGlass();

<liquid-glass stage="#stage" background="/bg.jpg"
  refraction="0.6" width="320" height="120" button>
  Play
</liquid-glass>
// React
import { LiquidGlassView } from 'liquid-glass/react';

<div ref={stageRef} style={{ position: 'relative' }}>
  <LiquidGlassView
    stage={stageRef} background="/bg.jpg"
    refraction={0.6} width={320} height={120} button>
    Play
  </LiquidGlassView>
</div>

Both wrappers take the same params and both renderers — background ⇒ WebGL, none ⇒ DOM. They’re thin shells over the same core.

5

Many panels, one draw — it scales

implemented in src/glass-pool.js

Every panel on the same stage shares one WebGL context and is drawn in a single drawArraysInstanced call — 500 panels cost about what a handful do. Panels scrolled offscreen are culled, DPR adapts to keep the GPU under budget, and the render loop halts entirely when nothing is animating.

// one shared texture, one context, one draw call for all of them
for (const p of spots) {
  const g = new LiquidGlass({
    stage, background: tex, renderer: 'webgl',
    width: 84, height: 54, x: p.x, y: p.y,
    refraction: 0.7, cornerRadius: 16,
  });
  stage.appendChild(g.element);
}
// add hundreds more — still one draw per frame
Chromium Firefox Safari
6

Interactive glass — press & drag

button / floating in src/liquid-glass.js

button: true brightens on hover and flattens the bevel + deepens the shadow on press. floating: true makes it draggable with a pointer. Grab the pill on the left and move it →

const g = new LiquidGlass({
  stage, background: tex, renderer: 'webgl',
  width: 150, height: 64, x: 24, y: 70,
  button: true,     // hover / press feedback
  floating: true,   // drag to move
});
g.content.textContent = 'Drag / press me';
Chromium Firefox Safari

Every parameter you can set

Pass any of these to the constructor, to glassify(), or live via glass.set({ … }). Values are clamped to the ranges shown. Full source: src/params.js.

paramdefaultrangewhat it does
refraction0.690 – 2how much the glass bends the image behind it
blurAmount0.00 – 1background blur (0 = sharp; taps scale with strength)
chromAberration0.050 – 1colour fringing at the edges
edgeHighlight0.050 – 2rim light / edge glow
specular0.00 – 2specular highlight (2-light Blinn-Phong)
fresnel1.00 – 2reflection at grazing angles
distortion0.00 – 1animated micro-distortion noise
cornerRadius650 – 2000corner radius, CSS px
zRadius401 – 2000bevel depth — curvature of the pill's cross-section
bevelMode00 or 10 = biconvex pill · 1 = dome / plano-convex
opacity1.00 – 1overall panel opacity
tintStrength0.00 – 1cool blue glass tint
saturation0.0-1 – 1saturation of the refracted image
brightness0.0-0.5 – 0.5brightness of the refracted image
shadowOpacity · shadowSpread · shadowOffsetY0.3 · 10 · 1drop shadow (compositor box-shadow)
button · floatingfalseboolhover/press feedback · drag-to-move

The API you call

import { LiquidGlass, glassify, detectDomCapabilities } from 'liquid-glass';

const g = new LiquidGlass({
  stage,                 // container the glass lives in (default document.body)
  background,            // img / video / canvas / URL → selects WebGL under 'auto'
  renderer: 'auto',     // 'auto' | 'webgl' | 'dom'  ('css' = alias for dom)
  width, height, x, y,   // geometry for a constructed panel
  maxDpr: 2,            // cap device-pixel-ratio for cost control
  dynamicCanvas: false,  // re-upload a canvas background every frame
  /* …any parameter from the table above… */
});

g.set({ refraction: 0.4 });   // patch params live
g.get('refraction');          // read one — or g.get() for all
g.setBackground(src);          // swap the WebGL texture
g.setPosition(x, y);           // move a constructed panel
g.destroy();                   // remove + free GPU resources
g.element;  g.content;            // the panel node · your content slot
g.mode;                           // 'webgl' | 'dom'
g.tier;                           // 'webgl' | 'displacement' | 'blur' | 'flat'

const panels = glassify('.card', {   // adopt existing element(s) → LiquidGlass[]
  keepBackground: false,           // true = keep the element's own background
  /* …params… */
});

detectDomCapabilities();          // { blur, displacement, tier } — before you commit

What is not possible (be honest with yourself)

Where each piece lives

See also: home · playground.html (WebGL playground) · glassflix.html (glassify over live content) · check.html (capability diagnostics).