Real WebGL liquid glass for the web — a tiny, framework-agnostic engine that makes any
DOM element refract the background behind it like a physical glass lens (bend + magnify + frost
+ chromatic edge + rim light), not just a backdrop-filter blur.
⚠️ Experimental. Early and evolving — the API, parameters, and visuals may change without notice, and it isn't production-hardened yet. See Limitations.
Building with an AI coding agent? This is the easiest path — install the skill, then just ask for liquid glass. Your agent gets the full API, the parameters, and the one rule, and wires it up correctly the first time:
npx skills add wiiiimm/glass-glWiring it by hand? No install needed — import straight from a CDN in any .html file (the
@latest tag always resolves to the newest release):
import { createGlass } from "https://esm.sh/glass-gl@latest";Using a bundler? npm i glass-gl instead. The full reference — the one rule, API, every
parameter, live backgrounds, gotchas — lives in
skills/glass-gl/SKILL.md.
One fullscreen WebGL <canvas> sits behind your page. Every frame, the engine reads each
registered element's on-screen rectangle and draws a glass lens at that spot — so the glass
tracks elements as they move, drag, or resize. Your content sits on the glass, crisp and
untouched; only the background gets bent.
Each lens runs this pipeline over the background you gave it:
- Rounded-box mask — the glass shape (an SDF rounded rectangle), matched to the element's
border-radius. - Droplet refraction — bends and magnifies the background;
curveshapes the profile (optically flat centre, steep bend at the rim). - Blur — a soft frost (
blur). - Chromatic dispersion — splits colour at the rim so white edges fringe into R/G/B, like real glass (
dispersion). - Vibrancy — saturates the refracted backdrop so it reads luminous (
saturation). - Rim light — a directional specular glint from the surface normal (
edgeLight+lightAngle), plus a frosted rim band (edgeFrost), and an optional milkytint.
A <canvas> or <video> background is live — re-uploaded every frame — so a playing video,
a Ken Burns slideshow, or anything you paint into a canvas refracts in real time. To make the
glass bend text or graphics, draw them into that background canvas (the lens only refracts
the background layer — see the one rule).
import { createGlass } from "https://esm.sh/glass-gl@latest";
const glass = createGlass({ canvas, background: "/bg.jpg" });
glass.register(el, { radius: 24 }); // any element becomes liquid glass (match its border-radius)
glass.setParams({ refraction: 0.22, blur: 1.2, dispersion: 0.2, curve: 2.8, tint: [1, 1, 1] });
glass.unregister(el); // stop treating el as glass
glass.setBackground("/other.jpg"); // string / <img> = static
glass.setBackground(videoEl); // <video> or <canvas> = LIVE, refracted every frame
glass.destroy(); // stop the render loop and release| Param | Range | What it does |
|---|---|---|
refraction |
~0–0.5 | Lens strength — how much the background bends / magnifies. |
blur |
px | Frost (sample spread). |
liquidness |
0–~0.6 | Milky mix toward tint (dark tint + liquidness = smoked glass). |
edgeLight |
~0–2 | Strength of the directional rim glint. |
edgeFrost |
0–1 | Frosted rim band. 0 = none. |
dispersion |
0–1 | Chromatic aberration at the rim (white → RGB fringe). Off by default. |
saturation |
~0.8–1.7 | Vibrancy of the refracted backdrop. 1 = neutral, ~1.2 = luminous. |
curve |
1–4 | Lens profile: 1 = linear bend, ~3 = droplet (flat centre, steep rim). |
lightAngle |
deg | Direction the rim glint comes from (0 = top, 90 = right). |
radius |
px | Default corner radius; override per element via register(el, { radius }). |
tint |
[r,g,b] 0–1 |
Glass colour ([1,1,1] = clear/light; dark values = dark/smoked glass). |
dpr— retina-sharp by default (renders atdevicePixelRatio, clamped to 2×). Pass a number to cap it, orfalsefor 1:1.transparent: true— draw only the lenses and leave every other pixel transparent, for when the background you refract is the page's own live canvas (e.g. a three.js scene) so the page stays crisp.size: () => ({ w, h })— size the canvas to a positioned/scrolled box instead of the viewport (e.g. an immersive iOS 26 Safari layout where the canvas covers a scroll band taller than the screen, so the background bleeds under the bars).
It bends a background, not the page. The shader only samples the background texture you
give it — a photo, video, gradient, or <canvas>. It has no access to your DOM, so it can
not refract arbitrary live HTML sitting behind it (that would mean rasterising the page every
frame — slow and CORS-bound, which is why even Apple's Liquid Glass runs in the OS compositor,
not the browser).
| Arrangement | Works? |
|---|---|
| Content (text, images) on top of a glass surface | ✅ yes |
| Glass refracting a background image / canvas / video / gradient | ✅ yes |
Glass refracting a live DOM element behind it (e.g. a scrolling <p>) |
❌ no — bake it into the background instead |
Want a frosted panel over live, scrolling content (a feed, an article)? Reach for the
browser's own backdrop-filter: blur() — it blurs real DOM, live and for free. It just can't
refract. Use glass-gl for glass over a media background: hero sections, photo/video
backdrops, draggable tiles, showpieces.
Beyond the one rule, know these before you reach for it:
- Rounded rectangles only. Every surface is a rounded-box lens (a circle is just
radius = size / 2). Arbitrary shapes — triangles, blobs — aren't supported; the shape is baked into the shader. - Up to 16 glass surfaces at once (the shader's array size). Register more and the extras are skipped.
- It runs a continuous render loop. One
requestAnimationFramekeeps the GPU awake so the glass tracks moving/resizing elements every frame. Fine for a desktop showpiece; for mobile or always-on UI, pause it when nothing's moving or the page is offscreen. - External images need CORS. A cross-origin background texture must be served with
Access-Control-Allow-Origin(and loadedcrossOrigin="anonymous"), or the WebGL upload fails. Same-origin / bundled images are simplest. - Needs WebGL.
createGlassthrows if a WebGL context can't be created — feature-detect or guard it if you support very old browsers. - Glass needs a busy/colourful background to read — put it over a photo, video, or gradient, never a flat colour.
- Experimental — not production-hardened; API and defaults may still change.
The playground/ directory is the live demo site (deployed at glass-gl.wiiiimm.codes),
and every page is a plain consumer of the same glass-gl.js you'd install:
index.html— the landing page: glass cards over a dark Ken Burns photo, pick one of the three experiences.os.html— glassOS. A desktop where everything is glass: menu bar, dock, and draggable app windows (About, How it works, Skills, Wallpapers, Settings, a Spotify Music player, a YouTube Video player, and a Terminal).playground.html— tune every parameter live (presets, sliders, copy-as-code). Add?transparentto see transparent mode over the page's own live canvas.mobile.html— an iPhone-style home screen of glass: draggable icons, a working music/video player, and a Camera app that feeds your real camera to the engine so every tile refracts the world. Immersive iOS 26 full-bleed layout.
cd playground && python3 -m http.server 8000
# open http://localhost:8000 (landing)
# /os.html · /playground.html · /mobile.html (the three experiences)glass-gl ships from packages/glass-gl, kept simple while the project is experimental:
- Bump the
versioninpackages/glass-gl/package.jsonand commit (Conventional Commits). - Run the Publish glass-gl workflow — GitHub → Actions → Run workflow, or
gh workflow run "Publish glass-gl". - CI publishes to npm and cuts a matching GitHub Release — only when that version isn't already on npm. It does not run on push, so a routine commit can't surprise-publish.
The engine lives once:
packages/glass-gl/glass-gl.jsis canonical andplayground/glass-gl.jsis a symlink to it — edit the package file. SeeAGENTS.mdfor the full layout and conventions.
AGPL-3.0-or-later © wiiiimm — see LICENSE. If you run a modified version as part of a network service, the AGPL requires you to offer its source to your users. (Versions ≤ 0.0.2 were published under MIT and remain so.)
