Realistic WebGL glass effects for any HTML element
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt.
Liquid Glass is a lightweight JavaScript/TypeScript library that applies realistic glass refraction, blur, chromatic aberration, and lighting effects to any HTML element using WebGL shaders. It captures the DOM content behind each glass element, processes it through a multi-pass rendering pipeline, and composites the result in real time.
The library handles layered compositing (glass-on-glass), dynamic content updates, draggable floating panels, responsive resizing, and works with any background — images, videos, canvases, or plain HTML.
Install via npm:
npm install liquid-glass
Basic usage:
import { LiquidGlass } from 'liquid-glass';
const instance = await LiquidGlass.init({
root: document.querySelector('#my-root'),
glassElements: document.querySelectorAll('.glass'),
});
// Later:
instance.destroy();
The root element must be a positioned container (position: relative) and each glass element must be a direct child of the root. Each glass element gets a child <canvas> injected automatically for the shader output.
Adjust the sliders to see how each parameter affects the glass effect in real time. Drag the glass panel to move it around.
Each glass element can be configured individually via data-config (JSON) or globally via the defaults option:
| Option | Type | Default | Description |
|---|---|---|---|
blurAmount | number | 0 | Background blur strength — softens / frosts the captured background (0–1) |
refraction | number | 0.69 | How much the glass bends light |
chromAberration | number | 0.05 | Color fringing at edges |
edgeHighlight | number | 0.05 | Inner glow / rim lighting |
specular | number | 0 | Specular highlight (Blinn-Phong) |
fresnel | number | 1 | Fresnel reflection at grazing angles |
cornerRadius | number | 65 | Corner radius in CSS px |
zRadius | number | 40 | Bevel depth (curvature) |
brightness | number | 0 | Brightness adjustment (-0.5 to 0.5) |
saturation | number | 0 | Saturation (-1 to 1) |
shadowOpacity | number | 0.3 | Drop shadow opacity |
floating | boolean | false | Enable drag-to-move |
button | boolean | false | Button mode (hover/press shader feedback) |
bevelMode | number | 0 | 0 = biconvex pill, 1 = dome (flat bottom) |
element.dataset.config = JSON.stringify({
blurAmount: 0.25,
cornerRadius: 30,
});
element.dataset.config = JSON.stringify({
brightness: -0.3,
blurAmount: 0.25,
cornerRadius: 50,
});
Set button: true to make a glass element react to hover and press: hovering brightens it, pressing flattens the bevel and deepens the shadow. Try the button below:
element.dataset.config = JSON.stringify({
button: true,
cornerRadius: 24,
});
Set bevelMode: 1 with equal cornerRadius and zRadius for a half-sphere lens effect. Try dragging the dome below:
element.dataset.config = JSON.stringify({
bevelMode: 1,
cornerRadius: 50,
zRadius: 50,
floating: true,
blurAmount: 0,
refraction: 1.2,
});
The library leans on real-time DOM rasterisation and a multi-pass WebGL pipeline. That comes with a handful of constraints worth knowing before you wire it into a production page.
LiquidGlass.init() call.<img>).<canvas> is injected as the glass element's first child for shader output. Avoid :first-child selectors on glass elements.html-to-image (style inlining + SVG-foreignObject decode). Keep wrappers small and shallow.data-dynamic elements are treated as always dirty by definition. Use it only for content that genuinely changes every frame — animations, counters, charts. For one-shot updates that don't happen every frame, prefer instance.markChanged() (see below) — it costs nothing on idle frames.font-size values for any text that may sit under glass. Sub-pixel sizes (e.g. 0.92em, clamp(...)) cause sub-pixel rounding differences between live HTML rendering and the SVG-foreignObject path used for capture, which shifts glyph positions inside the refraction.init() and served with CORS-friendly headers. Google Fonts, jsdelivr, and unpkg work out of the box. Self-hosted fonts on a same-origin server need no special config. Webfonts loaded after init will fall back to system fonts inside captured rasters.<img> elements need crossorigin="anonymous". Tainted canvases break texture upload and disable the glass effect for the entire root.LiquidGlass.init() is async — it resolves only after font CSS prefetch, glass content pre-capture, and static-content pre-warm have all completed. On a slow connection that can be 100–500 ms.data-dynamic only catches direct children of the root. A live element nested inside a wrapper that lacks data-dynamic will not trigger re-captures.<video> elements as dynamic — you don't need to add data-dynamic to them.html-to-image cannot rasterise <video> or <canvas>. The library handles them via a fast drawImage path instead.instance.markChanged()Call markChanged(element) after you update something the library can't observe on its own — a <canvas> you just painted, an <img> you just swapped, a CSS property you just toggled. Only glasses overlapping that element will re-render. Call with no argument to invalidate everything.
instance.markChanged(myCanvasElement); // targeted
instance.markChanged(); // everything