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: …
| Behind the glass is… | Use | Refraction | Implemented 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.
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.
| tier | blur | tint · bevel · sheen | refraction | chromatic aberration | you get it when… |
|---|---|---|---|---|---|
webgl | ✓ | ✓ | ✓ | ✓ | you pass a background texture |
displacement | ✓ | ✓ | ✓ | ✓ | glassify() on Chromium |
blur | ✓ | ✓ | ✗ | ✗ | glassify() on Firefox / Safari |
flat | ✗ | gradients only | ✗ | ✗ | no backdrop-filter at all |
src/glass-pool.js + src/shaders.jsYou 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';
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, });
src/dom-renderer.jsNo 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
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.
src/glass-pool.jsEvery 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
button / floating in src/liquid-glass.jsbutton: 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';
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.
| param | default | range | what it does |
|---|---|---|---|
refraction | 0.69 | 0 – 2 | how much the glass bends the image behind it |
blurAmount | 0.0 | 0 – 1 | background blur (0 = sharp; taps scale with strength) |
chromAberration | 0.05 | 0 – 1 | colour fringing at the edges |
edgeHighlight | 0.05 | 0 – 2 | rim light / edge glow |
specular | 0.0 | 0 – 2 | specular highlight (2-light Blinn-Phong) |
fresnel | 1.0 | 0 – 2 | reflection at grazing angles |
distortion | 0.0 | 0 – 1 | animated micro-distortion noise |
cornerRadius | 65 | 0 – 2000 | corner radius, CSS px |
zRadius | 40 | 1 – 2000 | bevel depth — curvature of the pill's cross-section |
bevelMode | 0 | 0 or 1 | 0 = biconvex pill · 1 = dome / plano-convex |
opacity | 1.0 | 0 – 1 | overall panel opacity |
tintStrength | 0.0 | 0 – 1 | cool blue glass tint |
saturation | 0.0 | -1 – 1 | saturation of the refracted image |
brightness | 0.0 | -0.5 – 0.5 | brightness of the refracted image |
shadowOpacity · shadowSpread · shadowOffsetY | 0.3 · 10 · 1 | — | drop shadow (compositor box-shadow) |
button · floating | false | bool | hover/press feedback · drag-to-move |
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
glassify() refracts the real DOM behind it
only on Blink; Firefox and Safari (all iOS browsers) fall back to blur. This is an engine limit, not a bug.<iframe>'s pixels.glassify() clears the element's own background by default (that opaque fill is what
you'd otherwise see instead of the page). Pass keepBackground: true to keep it.src/liquid-glass.js — the LiquidGlass class, glassify(), renderer selection, the adopt path.src/glass-pool.js — the WebGL renderer: one shared context, one instanced draw for every panel, culling, immutable texture upload.src/shaders.js — the GLSL: SDF mask, analytic bevel normal, refraction, chromatic aberration, adaptive blur.src/dom-renderer.js — the DOM renderer: baked displacement map + backdrop-filter, capability tiers (displacement / blur / flat).src/params.js — every tunable parameter, its range and default.src/web-component.js, src/react.js — the two wrappers.See also: home · playground.html (WebGL playground) · glassflix.html (glassify over live content) · check.html (capability diagnostics).