npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@zerocoder1/liquid-glass

v0.1.0

Published

Real-time liquid glass material for web, PWA and HTML5 TV. WebGL2 refraction with a CSS backdrop-filter fallback, plus glassify() to turn existing DOM into glass in place.

Readme

Liquid Glass

npm license: MIT live demo

A refracting glass material for the web. Two renderers, one API. Ships as ESM, CommonJS, and a plain-<script> global — so it drops into a React app, a vanilla HTML page, a PWA, or an HTML5 TV / webOS / Tizen set-top box.

Liquid Glass preview

See it live → zerocoder1.github.io/LiquidGlass

The home page shows the effect and links to the interactive playground, the implementation guide, and the demos.

Which renderer do I use?

One question decides it: is the thing behind the glass a texture, or the live page?

| Behind the glass is… | Use | Refraction | |---|---|---| | an image / video / canvas you can supply | new LiquidGlass({ background })WebGL | full — every browser (Firefox, Safari included) | | the live page (arbitrary scrolling DOM) | glassify(selector)DOM | Chromium ✓ · Firefox / Safari get frosted blur |

renderer: 'auto' (the default) picks WebGL when you passed a background, DOM otherwise. Force it with renderer: 'webgl' or renderer: 'dom'.

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. For cross-browser refraction, hand the glass a texture (WebGL). To adopt arbitrary live UI, use glassify() and accept blur off-Chromium.

Install

npm — React, bundlers, modern browsers:

npm install @zerocoder1/liquid-glass
import { LiquidGlass, glassify } from '@zerocoder1/liquid-glass';
import { LiquidGlassView } from '@zerocoder1/liquid-glass/react';
import { defineLiquidGlass } from '@zerocoder1/liquid-glass/web-component';

CDN / <script> tag — vanilla HTML, PWAs, and TV platforms (HTML5 TV / webOS / Tizen). No bundler needed; the global build is transpiled to ES2015 and exposes window.LiquidGlass:

<script src="https://cdn.jsdelivr.net/npm/@zerocoder1/liquid-glass/dist/liquid-glass.global.min.js"></script>
<script>
  const glass = new LiquidGlass({ stage, background });
  LiquidGlass.glassify('.card', { refraction: 0.6 });
  LiquidGlass.defineLiquidGlass();          // registers <liquid-glass>
</script>

Bundles in dist/: liquid-glass.esm.js, liquid-glass.cjs, liquid-glass.global.min.js, react.esm.js, plus .d.ts types. Rebuild with npm run build.

Usage

1. WebGL — refracts a texture you supply (works everywhere)

import { LiquidGlass } from '@zerocoder1/liquid-glass';

const glass = new LiquidGlass({
  stage,                              // position: relative container
  background: imageOrVideoOrCanvas,   // covers the stage; can be a URL
  renderer: 'webgl',                  // (auto-picks WebGL when background is set)
  width: 320, height: 120, x: 40, y: 40,
  refraction: 0.7, chromAberration: 0.08, blurAmount: 0.1,
});
stage.appendChild(glass.element);
glass.content.textContent = 'Play';   // put labels/icons in glass.content

For an animated canvas, pass dynamicCanvas: true so it's re-uploaded every frame. Video backgrounds are dynamic automatically.

2. glassify() — turn existing DOM into glass in place

import { glassify } from '@zerocoder1/liquid-glass';

// #topbar and .card are ordinary styled HTML already on the page
const panels = glassify('#topbar, .card', {
  refraction: 0.7, blurAmount: 0.14, edgeHighlight: 0.5,
});

glassify() keeps the element's children, layout, and click handlers, and clears its opaque background (pass keepBackground: true to keep it). It returns the created LiquidGlass instances so you can tune them live.

Refraction on Chromium only. Firefox and Safari (all iOS browsers) can't resolve SVG displacement inside backdrop-filter, so they get frosted blur plus the sheen/bevel/edge gradients. Read glass.tier to see what you got: 'webgl', 'displacement', 'blur', or 'flat'.

3. Web component

import { defineLiquidGlass } from '@zerocoder1/liquid-glass/web-component';
defineLiquidGlass();               // registers <liquid-glass> once
<liquid-glass stage="#stage" background="/bg.jpg"
              refraction="0.5" width="320" height="120" button>
  Play
</liquid-glass>

Numeric / boolean params map to attributes (camelCase → kebab-case, e.g. chromAberrationchrom-aberration). Registration is an explicit call, not an import side effect.

4. React

import { LiquidGlassView } from '@zerocoder1/liquid-glass/react';

<div ref={stageRef} style={{ position: 'relative' }}>
  <LiquidGlassView stage={stageRef} background="/bg.jpg"
                   refraction={0.5} width={320} height={120} button>
    Play
  </LiquidGlassView>
</div>

React and react-dom are optional peer dependencies.

API

import { LiquidGlass, glassify, detectDomCapabilities } from '@zerocoder1/liquid-glass';

const glass = new LiquidGlass({
  stage,                 // container (default: document.body)
  background,            // img / video / canvas / URL → auto-selects WebGL
  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 GPU cost control
  dynamicCanvas: false,  // re-upload a canvas background every frame
  className: '',         // extra class on the panel element
  keepBackground: false, // glassify: keep the adopted element's own background
  /* …any parameter from the table below… */
});

glass.set({ refraction: 0.4 }); // patch params live (throttled to next frame)
glass.get('refraction');        // read one — or glass.get() for all
glass.setBackground(src);       // swap the WebGL texture (image/canvas/video/URL)
glass.setPosition(x, y);        // move a constructed panel
glass.destroy();                // remove + free GPU resources

glass.element;   // the panel node — append it to your stage
glass.content;   // slot for foreground content (labels, icons)
glass.mode;      // 'webgl' | 'dom'
glass.tier;      // 'webgl' | 'displacement' | 'blur' | 'flat'

// Adopt existing DOM. Returns LiquidGlass[]. Accepts the same params.
const panels = glassify('.card', { refraction: 0.6, keepBackground: false });

// Probe capabilities before you commit — e.g. to pick a background strategy.
detectDomCapabilities(); // { blur, displacement, tier }

Parameters

Pass any of these to the constructor, to glassify(), or live via glass.set({ … }). Values are clamped to the ranges shown.

| 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; tap count scales 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 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 (CSS box-shadow on the panel) | | button | false | bool | hover brightens; press flattens the bevel + deepens the shadow | | floating | false | bool | drag-to-move via Pointer Events |

Browser support

| | blur · tint · bevel · sheen | refraction | chromatic aberration | |---|---|---|---| | WebGL renderer — any WebGL2 browser | ✅ | ✅ | ✅ | | glassify() on Chromium — Chrome / Edge / Brave / Opera / Chromium PWAs | ✅ | ✅ | ✅ | | glassify() on Safari — including all iOS browsers | ✅ | ❌ | ❌ | | glassify() on Firefox | ✅ | ❌ | ❌ | | No backdrop-filter at all | gradients only | ❌ | ❌ |

Safari and Firefox both report url() support for backdrop-filter but don't resolve the reference — and an unresolved url() would invalidate the whole filter and drop the blur too — so they're served the blur tier (frosted glass without refraction). For full refraction on those engines, hand the glass a background texture and use the WebGL renderer.

What is not possible

  • Live-page refraction is Chromium-only. Engine limit, not a bug.
  • WebGL can't sample arbitrary live DOM. It refracts a texture you supply. To refract page content cross-browser you'd have to snapshot that content into a texture yourself (expensive, and not truly live).
  • Cross-origin media taints the texture. An image/video/canvas from another origin without CORS can't be uploaded to WebGL — the panel silently renders without refraction. Serve same-origin or send CORS headers.
  • No true iOS-compositor parity. The web has no API to sample the live rendered backdrop as a texture; the two renderers here are the closest the platform allows.

Performance

  • Every 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 and excluded from the adaptive-DPR budget.
  • Surface normals are computed analytically from the SDF gradient: one distance evaluation per fragment (a finite-difference normal costs five).
  • Blur is a Poisson-disk sample whose tap count scales with blurAmount: a single fetch at 0, three taps for a light blur, up to eight for a heavy one.
  • Background textures use immutable storage (texStorage2D + texSubImage2D), so a video background does not reallocate GPU memory every frame.
  • The RAF loop halts entirely when nothing is animating.
  • Panels re-measure whenever they redraw (a parameter change, resize, drag, or animated background). If they live in their own scroll container that moves independently of the stage, trigger a redraw on scroll — e.g. el.addEventListener('scroll', () => glass.set({})).

License

MIT — see LICENSE.