
LiquidGlass is a WebGL-powered Liquid Glass effect library that applies realistic glass refraction, background blur, chromatic aberration, and multi-light specular effects to any HTML element on the page.
The library uses fragment shaders to render each glass panel directly onto an injected canvas, captures surrounding DOM content as a rasterized scene texture, and composites everything in the correct stacking order.
It works in all modern evergreen browsers and supports live video, animated content, and draggable floating panels out of the box.
Features:
- Physical refraction that bends the scene visible through the glass.
- Chromatic aberration at glass edges for color fringing typical of real glass.
- Fresnel reflection at grazing angles for a physically-based sheen.
- Multi-light specular highlights using a Blinn-Phong shading model.
- Per-panel Gaussian blur.
- Re-captures animated or counter elements every frame when flagged as dynamic.
- Draws image, canvas, and video elements directly via the Canvas 2D API for speed.
- Supports a button interaction mode that brightens on hover and flattens the bevel on press.
- Accepts a dome/plano-convex bevel mode for a half-sphere magnifier appearance.
- Recovers from WebGL context loss automatically.
How to use it:
1. Install and import liquidglass.
# NPM $ npm install @ybouane/liquidglass
import { LiquidGlass } from '@ybouane/liquidglass';<!-- Or from a CDN -->
<script type="module">
import { LiquidGlass } from 'https://cdn.jsdelivr.net/npm/@ybouane/liquidglass/dist/index.js';
</script>2. Build the HTML structure. The root contains the sampled scene and the glass panels. Static nodes can live beside dynamic nodes. The glass panel must stay at the top level of the root.
data-dynamic: Add to any direct child of the root whose content changes every frame. The library re-rasterizes it on every tick.data-config: JSON string of per-element configuration. Must decode to a plain object. Invalid JSON produces a console warning and the element uses instance defaults.
<div id="scene"> <!-- Background: a sibling inside the root, captured by the shader --> <img class="bg-image" src="cityscape.jpg" alt="Background"> <!-- Static content: captured once and cached --> <h1 class="hero-title">Liquid Glass Demo</h1> <!-- Animated content: add data-dynamic so it re-captures every frame --> <div class="live-clock" data-dynamic>12:00:00</div> <!-- The glass element: must be a direct child of the root --> <div class="glass-card">Card Content</div> </div>
3. Initialize the Liquid Glass effect.
// Grab the glass element and configure it via data-config
const card = document.querySelector('.glass-card');
card.dataset.config = JSON.stringify({
blurAmount: 0.3, // Background blur intensity (0–1)
refraction: 0.75, // How strongly the glass bends the scene behind it
cornerRadius: 48, // Rounded corner size in CSS pixels
floating: true, // Allow the user to drag the panel
});
// Init is async — it pre-fetches fonts and pre-warms all captures
const instance = await LiquidGlass.init({
root: document.querySelector('#scene'),
glassElements: [card],
});
// Check the render loop frame rate
console.log('Current FPS:', instance.fps);
// Tear down when done (e.g., component unmount or SPA route change)
// instance.destroy();4. You can apply glass to multiple elements at once and set default configuration values for the entire instance. Per-element data-config values override these defaults.
const instance = await LiquidGlass.init({
root: document.querySelector('#scene'),
// Apply glass to every element with the class
glassElements: document.querySelectorAll('.glass-panel'),
// Instance-wide defaults — individual elements can override these
defaults: {
cornerRadius: 32,
refraction: 0.65,
edgeHighlight: 0.08,
shadowOpacity: 0.25,
},
});5. The library tracks DOM mutations automatically, but it cannot detect when you repaint a <canvas> via getContext('2d'), swap an <img> src, or update a CSS background-image via JavaScript. Call instance.markChanged() for those cases.
// Repaint a canvas that sits behind a glass panel
const myChart = document.querySelector('#chart-canvas');
drawChartUpdate(myChart); // your custom draw call
// Tell LiquidGlass that this element's pixels changed
// Only the glass panels that overlap myChart will re-render
instance.markChanged(myChart);
// No argument: invalidates all glass panels on this instance
instance.markChanged();6. Available configuration options. Set these on a per-element basis via element.dataset.config = JSON.stringify({...}). The library re-reads data-config on every change via a MutationObserver.
blurAmount(number): Background blur strength.0is sharp,1is maximum blur. Default:0.00.refraction(number): Bending strength of the glass on the scene behind it. Default:0.69.chromAberration(number): Color fringing intensity at the glass edges. Default:0.05.edgeHighlight(number): Rim lighting glow intensity around the glass perimeter. Default:0.05.specular(number): Specular highlight intensity using a multi-light Blinn-Phong model. Default:0.00.fresnel(number): Fresnel reflection strength at grazing angles. Default:1.00.distortion(number): Micro-distortion noise applied across the glass surface. Default:0.00.cornerRadius(number): Corner rounding in CSS pixels. Default:65.zRadius(number): Bevel depth controlling the curvature of the glass pill cross-section. Default:40.opacity(number): Overall panel opacity from0to1. Default:1.00.saturation(number): Saturation shift.-1is grayscale,0is normal,1is vivid. Default:0.00.tintStrength(number): Intensity of a cool blue glass tint. Default:0.00.brightness(number): Brightness adjustment from-0.5to0.5. Default:0.00.shadowOpacity(number): Drop shadow opacity. Default:0.30.shadowSpread(number): Drop shadow spread in CSS pixels. Default:10.shadowOffsetY(number): Shadow vertical offset in CSS pixels. Default:1.floating(boolean): Activates drag-to-move via Pointer Events. Default:false.button(boolean): Button interaction mode. Hovering brightens the panel; pressing flattens the bevel and deepens the shadow. Default:false.bevelMode(0 | 1):0is the default biconvex pill shape.1is a dome or plano-convex shape. PairbevelMode: 1with matchingcornerRadiusandzRadiusvalues for a half-sphere magnifier effect. Default:0.
7. API methods.
// Initialize a LiquidGlass instance (async).
// Resolves after font prefetch, glass content pre-capture, and static pre-warm.
const instance = await LiquidGlass.init({
root: HTMLElement, // Required. Container element.
glassElements: NodeList, // Elements to apply the glass effect to.
defaults: {}, // Optional. Override default per-element config values.
});
// Read the current measured frames-per-second (updated once per second).
console.log(instance.fps);
// Stop the render loop, remove injected canvases, restore inline styles, and free WebGL.
instance.destroy();
// Manually flag a specific element as changed.
// Only glass panels overlapping the element will re-render on the next frame.
instance.markChanged(element);
// Flag all glass panels on this instance for a full re-render.
instance.markChanged();
// Call after dynamically loading new font stylesheets.
// Forces the next LiquidGlass.init() call to rebuild the embedded font cache.
invalidateFontEmbedCache();FAQs:
Q: Why does my glass element show the background but not my text overlay?
A: This is a stacking context issue. The library re-implements the CSS stacking spec, but it only detects specific triggers on direct children of the root. Check that your text overlay element has an explicit z-index plus a non-static position, or that it uses a detected property like transform.
Q: My custom canvas behind the glass doesn’t update. What’s happening?
A: The library cannot detect pixel changes made via getContext('2d') or WebGL on a <canvas> element. After each repaint, call instance.markChanged(myCanvas).
Q: Can I use LiquidGlass inside a React or Vue component?
A: Yes. Call LiquidGlass.init() inside a useEffect or mounted hook after the DOM renders. Store the returned instance and call instance.destroy() in the cleanup function.
Q: Why is the glass not rendering correctly over my Google Font?
A: Webfonts must be fully loaded before calling LiquidGlass.init(). The library pre-fetches font CSS to embed fonts into the SVG raster. If a font loads after init, the captured rasters fall back to system fonts. Wrap your init() call in a document.fonts.ready promise to prevent this.







