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

@goran.alkovic/liquid-glass

v1.2.0

Published

"Liquid Glass"-like refraction for any DOM element using a SVG displacement map-based backdrop-filter, without extra dependencies.

Readme

LiquidGlass

Apple-style "Liquid Glass" refraction for any DOM element - SVG displacement-map backdrop-filter, ported from the technique described in kube.io - Liquid Glass in the Browser.

  • Zero runtime dependencies, ~31 kB minified, TypeScript with full type declarations.
  • Optional React hook at liquid-glass/react (React 18+ peer).
  • Works from file:// (no build step needed on the consuming page).
  • Chromium only (SVG filters as backdrop-filter); unsupported browsers get a plain CSS blur/saturate fallback.
<div class="card">…</div>
import {create} from '@goran.alkovic/liquid-glass';

create(document.querySelector('.card')!);

How it works

  1. A bezel profile (surface height function) describes the glass curvature from the element border (full thickness) to the end of the bezel (flat).
  2. Snell–Descartes law converts local surface slope + thickness into a pixel displacement magnitude (pre-computed 128-step LUT).
  3. The displacement vector field is baked into an opaque PNG (R = X, G = Y, 128 = neutral), slope-bounded (maxDecay) so the rim pull never reverses - the field cannot fold or mirror the backdrop.
  4. The specular rim is a razor-thin (~1.6 px) one-sided line baked into the map's blue channel; inside the SVG filter it masks a hyper-saturated copy of the refracted backdrop, topped by a faint gray glint.
  5. The filter chain (blur → feImage → feDisplacementMap → rim compositing) is referenced from backdrop-filter: url(#id).

Tiles are baked 9-slice style and cached per shape, so resizes re-position the tiles (cheap attribute updates) instead of re-baking. On touch devices the default assembly: 'auto' bakes one full-size map sheet instead - a single feImage, roughly 2.5× fewer filter primitives per frame for mobile-class GPUs (at the cost of a re-bake on size changes). Elements sharing a filterId share a single baked filter.

Install

bun add @goran.alkovic/liquid-glass

Bundlers (ESM / CJS):

import {create} from '@goran.alkovic/liquid-glass';

Classic script (global LiquidGlass) - the IIFE build ships inside the npm package:

<script src="node_modules/@goran.alkovic/liquid-glass/dist/liquid-glass.min.js"></script>

Usage

import {create, applyAll} from '@goran.alkovic/liquid-glass';

// one element
const glass = create(document.querySelector('.card'), {
	surface: 'convex-squircle',
	bezel: 64,
	thickness: 52,
	specular: {angle: 65, saturation: 6, opacity: 0.4},
});

// every element matching a selector
applyAll('[data-liquid-glass]');

// live tuning (cheap options don't re-bake)
glass.setOptions({scale: 1.4, blur: 2, specular: {opacity: 0.6}});

// tear down
glass.destroy();

React

A hook ships at the liquid-glass/react subpath (React 18+ as an optional peer dependency - non-React consumers never install it):

import {useLiquidGlass} from '@goran.alkovic/liquid-glass/react';

function Card({strong}: {strong: boolean}) {
	const options = useMemo(() => ({thickness: strong ? 90 : 50}), [strong]);
	const {ref, glass, on, refresh, relayout} = useLiquidGlass(options);
	return <article ref={ref}>…</article>;
}

The controller is created when the node mounts and destroyed on unmount (React 18 StrictMode-safe). Options flow through setOptions - memoize the options object, identity is what triggers updates. on subscribes to typed events even before the instance exists (subscriptions are queued and re-attached across node swaps).

Events

el.addEventListener('lglass:refresh', (e) => {
	// e.detail: { shared, renderScale, debugUrl, width, height, maxDisplacement }
});
el.addEventListener('lglass:relayout', (e) => {
	// e.detail: { width, height }
});

Or subscribe type-safely on the instance - on returns an unsubscribe function, and event names/details are checked against the (open, declaration-mergeable) LiquidGlassEvents map:

const off = glass.on('lglass:refresh', (e) => console.log(e.detail.renderScale));
off();

Custom surfaces

surface accepts any registered profile name - register your own bezel cross-section with registerSurface (the profile name becomes part of the bake signature, so custom profiles never share cached tiles with built-ins):

import {registerSurface, listSurfaces} from '@goran.alkovic/liquid-glass';

registerSurface('prism', {
	f: (t) => 1 - (1 - t) * (1 - t), // thickness envelope, f(0)=0 → f(1)=1
	fp: (t) => 6 * (1 - t) * t, // slope driving Snell refraction
});

listSurfaces(); // ['convex-squircle', 'convex-circle', 'concave', 'lip', 'lens', 'prism']

Sharing one filter between identical elements

create(a, {filterId: 'toolbar'});
create(b, {filterId: 'toolbar'}); // attaches - no second bake

The group contract (size, corner radii, bake options) is validated; mismatching elements fall back to a private filter with a console warning.

Options

| Option | Default | Description | | ----------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | surface | 'convex-squircle' | Bezel profile: convex-squircle, convex-circle, concave, lip, lens (Apple-style edge wall + flat middle) - or any name added via registerSurface. | | bezel | 'auto' | Refracting edge width in px. 'auto' = min(48, min(w, h) / 2). | | thickness | 'auto' | Virtual glass thickness in px - the main strength knob. 'auto' = bezel. | | ior | 1.5 | Refractive index. | | scale | 1 | Displacement multiplier. Cheap (no re-bake). | | saturate | 1 | Global saturation of the refracted backdrop. Cheap. | | tint | 0 | White "material" tint over the refracted backdrop (0–1) - the translucent, milky Apple-style look. Pairs with blur/saturate. Cheap. | | scrim | 0 | Legibility scrim (0–1): darkens bright backdrop regions so white content on top of ultra-clear glass keeps contrast. Cheap. | | shrink | 0 | Fraction the backdrop is pulled toward the element center (0–0.5) - the Apple-style scale-down. A tiny gradient displacement pass, exact at any size. Cheap. | | blur | 0 | Pre-displacement backdrop blur (px). Cheap. | | smooth | 0.15 | Post-displacement micro-blur (px) - smooths rendered refraction grain. Keep ≤ ~0.5. Cheap. | | dispersion | 0 | Chromatic aberration, 0–0.5 - red/blue displaced slightly off green (three passes). Cheap. | | settle | 0 | ms. While geometry churns, keep the last filter (stretched) and debounce the re-bake until quiet this long. No-op for static. | | refreshOnResize | 0 | ms. Opt-in for static: also watch size changes and re-bake (debounced) once they go quiet - e.g. rotation. 0 = off. | | maxDecay | 1 | Displacement decay bound (px/px). 1 = fold-free rim by design; 0 = uncapped reference look. Affects the bake. | | specular.angle | 65 | Light travel direction, degrees (0° = from left, 90° = from top). | | specular.saturation | 6 | Saturation boost inside the rim line only. Cheap. | | specular.opacity | 0.4 | Opacity of the gray glint line. Cheap. | | specular.width | 1.6 | Rim line width (px). | | specular.gray | 120 | Gray level of the glint color. | | specular.ambient | 0 | Base rim intensity visible all around the border, regardless of light (0–1) - brighter on the lit side. Affects the bake. | | specular.glow | 0 | Broad, faint white bloom just inside the border (0–1) - the plump "pillow" glow. Affects the bake. | | specular.innerOpacity | 0 | Opacity of a second, wider inner ring (0–1) - the Apple-style double edge. Affects the bake. | | specular.ambient | 0 | Base rim intensity visible all around the border, regardless of light (0–1) - brighter on the lit side. Affects the bake. | | specular.glow | 0 | Broad, faint white bloom just inside the border (0–1) - the plump "pillow" glow. Affects the bake. | | filterId | null | Share one baked filter across identical elements. | | renderScale | 'auto' | Bake resolution multiplier (quality). 'auto' = 1.25× desktop / 1× touch / 0.15× dense touch screens (DPR ≥ 2 - the map is upscaled to device pixels there anyway). Explicit numbers clamp to [0.15, 4]. | | assembly | 'auto' | Map delivery: 'tiles' = 9-slice images in the filter (resizes are cheap re-layouts); 'sheet' = one full-size map feImage (~2.5× fewer per-frame primitives, sizes re-bake). 'auto' = sheet on touch devices and for static instances. | | throttleMs | 120 | Throttle for re-bakes on geometry changes. | | fallback | 'blur(10px) saturate(1.5)' | backdrop-filter value for unsupported browsers. | | debug | false | Also bake an inspectable debug map (mapDebugDataUrl). | | static | false | Generate once for the creation geometry - no observers, no automatic updates (see below). | | supportedClass | null | Class(es) added when SVG backdrop-filter is supported - string or array. | | fallbackClass | null | Class(es) added when the CSS fallback is active - string or array. |

Static glass

For elements whose size is known and fixed, static: true skips the observers entirely: the effect is generated once for the geometry at creation, and no automatic re-bakes or re-layouts ever run - zero ongoing work after setup. Since the cheap resize re-layout would go unused, static instances (outside a filterId group) also bake as a sheet under 'auto' assembly - the fewest-primitive filter, for the element's whole lifetime.

create(document.querySelector('.badge'), {static: true});

Cheap setOptions (scale, saturate, blur, specular opacity/saturation) still applies - handy for hover effects - and refresh() can be called manually after a known layout change. If the element is hidden (zero size) at creation, call refresh() once it becomes visible. For elements that only ever resize (rare), refreshOnResize re-bakes - debounced - once the resizing has been quiet. In a filterId group the owner's updates still propagate to static followers.

Standalone map generation

import {generateMaps, generateTiles} from '@goran.alkovic/liquid-glass';

const maps = generateMaps({width: 420, height: 56, borderRadius: 28});
// maps.mapDataUrl / maps.specularDataUrl - feed your own filter chain

const tiles = generateTiles({borderRadius: 28}); // 9-slice, size-independent

Static filters

Need just the SVG filter - no element controller, no observers? Generate one for a border radius and reference it from CSS yourself:

import {createFilter} from '@goran.alkovic/liquid-glass';

const f = createFilter({borderRadius: 28, width: 420, height: 56});
el.style.backdropFilter = f.url; // Chromium: full refraction

f.layout(480, 64); // cheap re-layout for a new size (no re-bake)
f.destroy(); // remove the <filter> from the DOM

Returns { id, url, element, maxDisplacement, layout(), destroy() }. Tiles come from the same cache as the element controllers, so identical geometries share bakes. As backdrop-filter this is Chromium-only; as a regular filter it works anywhere SVG filters do.

Browser support

import {isSupported} from '@goran.alkovic/liquid-glass';
if (!isSupported()) {
	/* the CSS fallback is active */
}

Or let the library tag your elements - supportedClass / fallbackClass are added/removed automatically (and swapped if detection changes), so you can branch your CSS on capability:

create(el, {supportedClass: 'glass-ok glass-ready', fallbackClass: ['glass-fallback', 'no-refraction']});

Safari (in progress)

WebKit has an implementation of backdrop-filter: url() reference filters in review (bug 245510, PRs 68613/68614/69566). Detection is version-gated for it, so the library stays on the CSS fallback until the shipping Safari version is confirmed. To test a Technology Preview that carries the work today:

import {forceSupported} from '@goran.alkovic/liquid-glass';
forceSupported(true); // false disables, null returns to sniffing

Firefox

Firefox's WebRender silently drops backdrop-filter when it would need an SVG filter graph (bug 1961378), so those browsers get the CSS fallback. (An interoperable spec solution - a BackdropGraphic filter input - is being discussed in svgwg#1142.)

Development

bun install
bun run build # typecheck + lint + dist (esm, cjs, iife, iife.min, .d.ts)
bun run dev # watch the browser (IIFE) bundle
bun run check # typecheck only
bun run lint # oxlint
bun run format # prettier

Open examples/ for the interactive React demo - served by Bun standalone (HTML imports + HMR, no bundler config):

cd examples
bun install
bun run dev # http://localhost:5173 (or from the repo root: bun start / bun run example)
bun run build # static production build → examples/dist

The dev server binds every interface and prints Network: URLs, so the demo can be previewed from phones or other machines on the LAN - useful for checking the glass on real device pixels. PORT and HOST env vars override the port/interface (HOST=127.0.0.1 keeps it local-only); Windows Firewall may need to allow bun for private networks on first run.

examples/src/Glass.tsx is a small wrapper component built on the hook (create on mount, setOptions on prop changes, destroy on unmount) - copy it into your app or adapt it. Note bun's watcher only covers the project directory it runs from: bun run example from the repo root watches both the app and the library source.

Source layout (src/): types (public types) · options · surfaces (the profile registry + registerSurface) · sampler · bake · cache · geometry · svg · filter (the SVG <filter> node + slice layout) · static-filter (createFilter) · react (the useLiquidGlass hook) · controller (the LiquidGlass class) - everything below the controller is pure (or canvas-only) and unit-testable in isolation.

License

MIT