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

apple-liquid-glass-webgl

v1.0.1

Published

Reusable WebGL2 liquid glass surfaces for folders, rectangles, pills, and circles.

Readme

apple-liquid-glass-webgl

Reusable WebGL2 liquid glass surfaces for folders, rectangles, pills, and circles.

This package is framework-free and renders Apple-inspired translucent surfaces with screen-space refraction, variable blur, Fresnel reflection, chromatic dispersion, edge highlights, and contact shadows.

Install

npm install apple-liquid-glass-webgl

WebGL2 is required. Give the canvas a CSS width and height before rendering.

Check support before constructing, so a browser without WebGL2 can fall back instead of catching a constructor throw:

if (LiquidGlassWebGL.isSupported()) {
  const glass = new LiquidGlassWebGL(canvas);
} else {
  panel.classList.add('css-fallback');
}

Usage

import { LiquidGlassWebGL } from 'apple-liquid-glass-webgl';

const canvas = document.querySelector('canvas');
const glass = new LiquidGlassWebGL(canvas, {
  material: 'regular',
  fusion: true,
});

await glass.setWallpaper('/images/wallpaper.jpg');
glass.setElements([
  { id: 'folder', shape: 'folder', x: 80, y: 80, width: 220, height: 220 },
  { id: 'rect', shape: 'rect', x: 360, y: 100, width: 280, height: 190 },
  { id: 'pill', shape: 'pill', x: 700, y: 130, width: 250, height: 110 },
  { id: 'circle', shape: 'circle', x: 1000, y: 130, size: 110 },
]);
glass.render();

The component accepts CSS-pixel coordinates. Content such as app icons, labels, or buttons can be drawn in a separate canvas layer above the WebGL canvas.

Default material parameters

getDefaultMaterial() returns a fresh copy of the package's default material parameters on every call. This makes it safe to customize the result without mutating the package defaults.

import { getDefaultMaterial, LiquidGlassWebGL } from 'apple-liquid-glass-webgl';

const material = getDefaultMaterial();
material.blurRim = 32;

const glass = new LiquidGlassWebGL(canvas, { material });

makeMaterial() with no argument is also equivalent to getDefaultMaterial(). The exported DEFAULT_MATERIAL constant contains the same values for read-only inspection.

| Group | Parameter | Default | | --- | --- | ---: | | Shape | radius | 64.00 | | Shape | squircle | 2.00 | | Shape | mergeRadius | 52.00 | | Shape | bevel | 34.00 | | Shape | height | 21.00 | | Optics | ior | 2.00 | | Optics | dispersion | 0.06 | | Optics | refractScale | 3.00 | | Optics | meniscus | 1.00 | | Optics | blurPlateau | 8.00 | | Optics | blurRim | 48.00 | | Optics | opticalDensity | 0.65 | | Lighting | specular | 0.89 | | Lighting | specPower | 11.50 | | Lighting | highlightAdapt | 0.83 | | Lighting | highlightWidth | 0.76 | | Lighting | highlightSharpness | 0.55 | | Lighting | highlightBase | 0.30 | | Lighting | fresnel | 0.65 | | Lighting | saturation | 1.35 | | Lighting | brightness | 0.00 | | Lighting | tintAmount | 0.02 | | Lighting | tintAdapt | 0.14 | | Edge | shadow | 0.09 | | Edge | shadowSize | 4.00 | | Edge | shadowOffset | 0.00 | | Edge | lightX | -0.18 | | Edge | lightY | 0.08 | | Edge | edgeLine | 0.30 | | Edge | edgeWidth | 0.50 | | Edge | edgeDark | 0.02 |

The returned object also includes tintColor: [1, 1, 1] and debug: 0. Parameter names use the JavaScript API names; for example, highlightAdapt is the “Light adaptation” control and edgeLine is the “Edge highlight” control.

Backdrop RGB is stored in SRGB8_ALPHA8: image uploads decode to linear light, every downsample and tent-upsample pass filters linear radiance, and writes encode back to sRGB. Alpha remains linear for the optical-density channel. Wide blur blends in the reconstructed chain to avoid coarse-mip breathing; final glass output receives a sub-LSB triangular dither to suppress dark-gradient banding. tintAdapt controls the component-level light/dark material switch (0 keeps tintColor fixed, 1 fully follows the backdrop below the component).

Live backdrops and overlay mode

Use compositeMode: 'overlay' when the original backdrop remains visible underneath the WebGL canvas. Pixels outside the glass stay transparent, while the supplied backdrop source is sampled for refraction and blur.

const glass = new LiquidGlassWebGL(canvas, {
  compositeMode: 'overlay',
  elements: [
    { id: 'panel', shape: 'rect', x: 80, y: 80, width: 520, height: 360 },
  ],
});

// Canvas, OffscreenCanvas, and video sources are detected as live. The
// renderer starts automatically and uploads their latest frame before drawing.
glass.setBackdrop(animatedCanvas);

// Stop the render loop when the view is hidden or unmounted.
glass.stop();

Static image sources upload once:

await glass.loadBackdrop('/images/wallpaper.jpg');

The update behavior can be selected explicitly:

glass.setBackdrop(source, { update: 'live' });
glass.setBackdrop(source, { update: 'static', autoStart: false });
glass.updateBackdrop(); // manually upload the latest static-source pixels
glass.start();
glass.stop();

The default compositeMode: 'replace' preserves the original behavior and draws the supplied backdrop across the full WebGL canvas. Browsers do not expose arbitrary composited DOM/CSS pixels to WebGL, so the backdrop must be supplied explicitly as an image, canvas, video, ImageBitmap, or OffscreenCanvas. Cross-origin sources must permit CORS access.

Visual preview

These screenshots are captured from the playground with the inspector hidden. Each scene uses the same folder, rect, pill, and circle surfaces:

Smooth-union fusion

Nearby components can share one continuous distance field, so the silhouette, refraction, highlights, and shadow flow through the merged surface.

Smooth-union liquid glass fusion

Individual scene previews

| Natural landscape | Abstract lines | | --- | --- | | Natural landscape | Abstract lines |

| Color blocks | Night city | | --- | --- | | Color blocks | Night city |

Hit testing

hitTest() evaluates the same signed distance field as the shader, so a pointer lands on the shape rather than on its bounding box: the corners of a circle are not clickable, and inside a fused group the bridge between two components is.

canvas.addEventListener('pointerdown', (event) => {
  const element = glass.hitTestEvent(event); // null outside the surface
  if (element) startDragging(element.id);
});

const { x, y } = glass.pointerPosition(event); // canvas-relative CSS pixels
glass.hitTest(x, y, { tolerance: 8 });         // slack for coarse pointers
glass.distanceAt(x, y);                        // signed distance, negative inside

A gap between two components only closes into a bridge while it is narrower than about half the fusion distance; past that, mergeRadius only softens the approach.

Rendering behaviour

render() returns without touching the GPU when nothing changed since the last frame, so an animation loop over a static scene is free. Every mutator marks the component dirty; a live backdrop always redraws. The sampled backdrop and its mip chain have a separate dirty flag, so moving a shape or changing its material only redraws the visible glass passes.

glass.render();                  // no-op when clean
glass.render({ force: true });   // always draws, for pixel read-back
glass.markDirty();               // after mutating glass.material in place
glass.markBackdropDirty();       // after changing a backdrop source in place

Pass preserveDrawingBuffer: true if you read the canvas back with readPixels or toDataURL after the frame has been composited.

Context loss and accessibility

A GPU context can be lost at any time. The component takes over recovery: the canvas holds its last frame, every call is a safe no-op while the context is gone, and programs, render targets and backdrop textures are rebuilt when the browser restores it.

const glass = new LiquidGlassWebGL(canvas, {
  onContextLost: () => showPlaceholder(),
  onContextRestored: () => hidePlaceholder(),
});

glass.contextLost; // true while the surface is frozen

Under prefers-reduced-transparency: reduce the material falls back to a near-opaque surface: refraction, dispersion and scattering are removed while the shape, edge and shadow stay. Opt out with respectReducedTransparency: false, and read glass.effectiveMaterial for the parameters actually in use.

autoResize (on by default) redraws when the canvas element is resized, which dirty tracking would otherwise miss in an app that renders on demand.

API

LiquidGlassWebGL.isSupported();

glass.setMaterial('clear');
glass.setMaterial({ blurPlateau: 4, edgeLine: 0.2 });
glass.setBackdrop(animatedCanvas, { update: 'live' });
glass.updateBackdrop();
glass.start();
glass.stop();
glass.setFusion(true, 52); // smooth-union distance in CSS pixels
glass.setWallpaperIndex(0);
glass.addElement({ id: 'new-folder', shape: 'folder', x: 20, y: 20, size: 180 });
glass.updateElement('new-folder', { x: 40 });
glass.removeElement('new-folder');
glass.hitTest(x, y);
glass.resize();
glass.destroy();

Available presets are regular, clear, and lens. Available shapes are folder, rect, pill, and circle. With fusion enabled, nearby elements are evaluated as one smooth-union distance field, so their silhouette, normals, refraction, highlights, and shadow merge continuously.

The shader carries 16 shapes per pass. Elements too far apart to influence each other are split into separate passes automatically, so the 16 shape limit applies per fused cluster rather than per scene; a cluster larger than that is still split, and logs a warning explaining that the silhouette will not bridge across every one of them.

The geometry helpers behind all of this are exported for use without a canvas — sdGroup, hitTestElements, connectedElementGroups and groupElements.

Playground

The interactive demo used to develop the material is deployed at oliverrr2424.github.io/webgl-apple-liquid-glass, or run it locally:

npm install
npm run serve

Open http://localhost:8765. It drives the published component through its public API, and the inspector covers:

  • Eight scenes: four wallpapers, plus a tab bar over app content, a notification, a control-centre grid, and a scrolling feed that exercises the live backdrop path.
  • Component editing: add, retype, resize and delete surfaces; drag them, or select one and use the arrow keys (Shift for ten pixels, Alt to resize, [ and ] to cycle, Delete to remove).
  • Every material parameter as both a slider and a typed value. Double click a parameter name to reset just that one; modified parameters are marked.
  • Copy link puts the whole session in the URL, Copy code emits the snippet that reproduces it.
  • A frame-rate, CPU-per-frame, drawing-buffer and pass-count readout. A static scene reports idle, because dirty tracking skips the GPU entirely.
  • Local image or looping-video uploads, and the thickness, normals and dispersion debug outputs.

Development

npm test                     # unit tests, then the browser test pages
npm run test:visual          # golden image comparison
npm run test:visual:update   # record a baseline for this renderer
npm run shot /tmp/liquid-glass.png -- --scene 0 --size 1200x720 --no-panel
npm run pack:check

tests/*.test.mjs are Node unit tests over the geometry and material rules. tests/*.html are browser pages, each exporting window.runTest(); they cover live and static backdrops, context loss and recovery, and the dirty-tracking contract by counting draw calls.

Visual regression forces ANGLE's deterministic SwiftShader backend. Baselines live in shots/baseline/<renderer>/; Linux's Subzero JIT and macOS's LLVM JIT have separate golden sets because a small number of edge pixels round differently. A missing baseline fails the run, and rejected frames are written to shots/tmp-*.png.

Automated npm publishing

Every push to main runs .github/workflows/ci.yml and then .github/workflows/publish-npm.yml, which publishes with provenance when the version in package.json is newer than the version already on npm. Publishing depends on the test job, so a failing test blocks the release.

To enable publishing, add a repository secret named NPM_TOKEN containing an npm token with permission to publish apple-liquid-glass-webgl. Bump the package version before pushing a release to main.

License

MIT