LiquidGlass renders your DOM to a canvas and refracts it with WebGL
A liquid glass effect library for the web. Apply realistic glass refraction, blur, chromatic aberration, and lighting effects to any HTML element using WebGL shaders.
At a glance
- What is it?
- LiquidGlass is a TypeScript library that rasterises the content behind a panel, crops the scene, and runs a fragment shader for refraction, chromatic aberration, Fresnel reflection and specular highlights, with per-panel configuration carried in a data attribute.
- Who is it for?
- LiquidGlass fits a design that needs a convincing glass panel over photographic or video content and can afford a per-frame rasterisation cost, and the per-element data-config means you can prototype several treatments in one page without rebuilding the bundle.
- Can I use it commercially?
- Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
- Is it still maintained?
- Yes. The repository last received commits 23 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The page is rasterised to a canvas, not blurred in the DOM
The technique behind LiquidGlass is not a CSS filter. Non-glass children of the root element are rasterised onto a hidden canvas using html-to-image, which clones the subtree, inlines the computed styles and renders through an SVG foreignObject. The result is a picture of what sits behind the panel, and that picture is what the shader refracts.
Media elements take a different path. Images, canvases and videos are drawn directly with ctx.drawImage, which is faster than going through html-to-image and, for video, the only way to capture live frames.
The split between the two paths has a cost you should know before choosing this approach. The html-to-image route is a full DOM clone with computed styles inlined, and for static children that is paid once and cached, so the static content around a panel is cheap. The cost scales with how much of the page is live, and it is per frame for anything you mark as dynamic.
The glass is a fragment shader over a cropped copy of the scene
Glass elements receive an injected child canvas that displays the WebGL output. For each glass element, the renderer crops the scene at the panel's location, runs an optional Gaussian blur, and then runs a fragment shader.
The shader is where the effect lives, and the list of what it applies is specific: refraction, chromatic aberration, Fresnel reflection, multi-light specular highlights, an inner-stroke rim, and a drop shadow. Each of those is a separate term in the shader rather than a separate element or filter, which is why the result can look like a lit physical object rather than a blurred rectangle.
The visual vocabulary is the glass language popularised by recent operating system designs, and the repository topics list it as glassmorphism and ios26-liquid-glass among others. The library is written in TypeScript and built with tsc plus esbuild into an ESM bundle, with the source and the dist directory both published in the package files list, so the shader source is readable in the installed package.
Stacked panels see each other because the canvas is composited back
Layered compositing is the fourth and final stage of the pipeline. Each rendered glass canvas is written back to the compositing canvas before the next glass element runs, so a glass element above another one sees the lower one in its refraction.
Without that step the effect breaks in an obvious way. A panel over a panel would refract only the original page content, and the lower panel would look like a hole, since the upper panel's shader has no knowledge of what was drawn into it. Writing the result back makes the upper panel treat the lower panel as part of the scene, which is what a physical stack of glass would do.
The ordering consequence is worth noting for your own layout. The elements are processed in sequence, and each one depends on the accumulated canvas state, so the visual result of a stack depends on the order in which the glass elements are given to the renderer rather than on the z-index alone.
data-dynamic is the price of animating what sits behind the glass
Static children of the root are captured once and cached. A child marked with data-dynamic, or any video element, is re-captured every frame, which is what makes an animated counter or a live video visible through the panel.
The data-dynamic attribute is added to a direct child of the root whose contents change every frame, and the README is explicit about its cost. Such elements are treated as always dirty by definition, so the library re-rasterises them every frame and re-runs the shader for every glass that overlaps them. The advice is to use it sparingly, because it is the only thing on the page that defeats the per-element dirty-tracking optimisation.
Video elements are auto-detected as dynamic, so a video needs no attribute at all. The same optimisation applies elsewhere in the library, where dirty tracking decides which panels need to be redrawn, and the per-frame re-capture of a dynamic child is the one case that overrides it.
Per element configuration is a JSON string in a data attribute
Individual panels are configured through a data-config attribute holding a JSON string, and the library re-reads it whenever it changes through a MutationObserver, so you can update a panel's treatment at runtime:
element.dataset.config = JSON.stringify({
blurAmount: 0.25,
floating: true,
cornerRadius: 40,
});The option set is wide, and the defaults matter as much as the names. Blur strength is blurAmount, defaulting to 0, refraction defaults to 0.69, chromatic aberration and edge highlight to 0.05 each, and Fresnel reflection to 1.00, while specular highlights and micro-distortion noise start at zero. Shape comes from cornerRadius at 65 CSS pixels and zRadius at 40, which is the bevel depth controlling the curvature of the pill's cross-section.
Two options change behaviour rather than appearance. Floating enables drag-to-move through Pointer Events, and button mode brightens the panel on hover while flattening the bevel and deepening the shadow on press. There is also a bevelMode switch, where 0 is a biconvex pill and 1 is a dome or plano-convex shape, which paired with cornerRadius equal to zRadius gives a half-sphere magnifier.
init resolves only after fonts and content are ready
LiquidGlass.init is asynchronous and returns a promise for the instance. It resolves once the page's webfonts have been pre-fetched and every glass element's content has been pre-captured, which is the part that surprises people who expect an immediate return.
The options are root, which is required and must be the direct parent of the glass elements, glassElements as a NodeList or an array, and defaults, which overrides the default per-element configuration for that instance:
const instance = await LiquidGlass.init({
root: document.querySelector('#root'),
glassElements: document.querySelectorAll('.glass'),
defaults: {
cornerRadius: 24,
refraction: 0.8,
},
});The instance exposes a measured frames per second value updated once per second, a destroy method that stops the render loop, removes the injected canvases, restores mutated inline styles and frees WebGL resources, and markChanged for content the library cannot observe on its own. The package also exports invalidateFontEmbedCache, which you call after loading new font stylesheets dynamically so the next init rebuilds the embedded font cache.
Installing it is one command or one script tag
The package is published on npm under the scoped name @ybouane/liquidglass, and the install is:
npm install @ybouane/liquidglassThe README also offers a route that skips the install entirely, importing the ESM build straight from a CDN:
<script type="module">
import { LiquidGlass } from 'https://cdn.jsdelivr.net/npm/@ybouane/liquidglass/dist/index.js';
</script>That CDN path is useful for a quick trial and for a demo page, since the dist entry point is what the package's exports map points to. For production you would use the npm install, and the package manifest is explicit about the shape of what you get: an ESM-only module with a single export condition, type declarations alongside the JavaScript, and both dist and src included in the published files.
One manifest detail deserves attention in a security review. The package defines a postinstall script that runs patch-package, and the repository carries a patches directory, so installing the package applies patches to a dependency at install time. That is a deliberate choice by the author, and it is the kind of thing worth looking at before the package goes into a production dependency tree.
Where the effect stops being worth it
The obvious case where this library is the wrong tool is a page that does not need it. A translucent panel over flat colour is a background and a backdrop filter, and the WebGL path adds a rasterisation pipeline to solve a problem CSS already solves. The library is for a panel whose content has to look refracted and lit.
The harder case is heavy animated content behind the glass. Every data-dynamic child is re-rasterised each frame through a DOM clone and an SVG foreignObject render, and every overlapping glass panel re-runs its shader against that result. A live video under a panel is the supported case, since video is drawn directly, but a canvas of animated DOM is the expensive one, and the README's own instruction to use data-dynamic sparingly is the warning.
There is also the accessibility and semantics question, which the library does not address. The panel is a canvas injected into your markup, so the text inside it still has to be real DOM for a screen reader, and the visual treatment does not change what assistive technology reports. The project publishes no accessibility documentation, so treat the visual effect as decoration layered on top of markup you keep accessible yourself.
Editorial conclusion
LiquidGlass fits a design that needs a convincing glass panel over photographic or video content and can afford a per-frame rasterisation cost, and the per-element data-config means you can prototype several treatments in one page without rebuilding the bundle. It does not fit a page with heavy animated DOM behind the glass, since a data-dynamic child is re-rasterised every frame through html-to-image, and it is the wrong tool when you need a simple translucent background that CSS can do. Before you ship it, check three things: that the glass elements are direct children of the root element, that the library is initialised after webfonts load or the pre-captured text will be wrong, and that you call destroy on teardown, since it is what removes the injected canvases and restores the mutated inline styles. The package version is 1.0.3, the repository publishes no GitHub releases, and the last push was on 2026-09-30.
Frequently asked questions
How do I install and use the LiquidGlass library?
Install it with `npm install @ybouane/liquidglass`, or import the ESM build from a CDN. Then mark the elements that should look like glass, set their `data-config` if you want to adjust the effect, and call `LiquidGlass.init` with the root element and the glass elements.
What does LiquidGlass do with the content behind a panel?
Non-glass children of the root are rasterised onto a hidden canvas with html-to-image, which clones the subtree and renders it through an SVG foreignObject, while images, canvases and videos are drawn directly with ctx.drawImage. The renderer then crops the scene at the panel and runs the shader over it.
What is the data-dynamic attribute for in LiquidGlass?
You add it to a direct child of the root whose contents change every frame, such as a counter or a chart. Those elements are re-captured every frame, and the README warns that this is the only thing that defeats the per-element dirty-tracking optimisation, so it should be used sparingly.
How do I configure the glass effect for one element?
Set a JSON string in the element's `data-config` attribute, for example with blurAmount, floating and cornerRadius keys. The library re-reads the attribute whenever it changes through a MutationObserver, so the effect can be updated at runtime.
What does LiquidGlass.init return and how do I clean up?
It is asynchronous and resolves to the instance once webfonts are pre-fetched and glass content is pre-captured. The instance exposes the measured frames per second, a markChanged method for content the library cannot observe, and a destroy method that stops the render loop, removes injected canvases, restores mutated inline styles and frees WebGL resources.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ybouane-liquidglass)Was this useful?