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

@el-gladiador/liquid-glass-react

v0.1.1

Published

High-performance liquid glass / iOS-26 style refraction effects for React, powered by WebGL2 with multi-layer refraction and a real Gaussian blur kernel.

Readme

liquid-glass-react

A React component for iOS-26-style liquid glass / refraction effects. Single shared WebGL2 context, three-layer refraction (edge / rim / centre), real 13-tap Gaussian blur sampled in-shader, and ambient colour pickup from the surrounding background. No per-element canvas, no per-frame allocation, no requestAnimationFrame spinning when nothing's changed.

Install

npm install @el-gladiador/liquid-glass-react

html-to-image is bundled as a dependency. react and react-dom are peer deps (>=18).

Quick start

import { LiquidGlassProvider, LiquidGlass } from '@el-gladiador/liquid-glass-react';

export default function App() {
  return (
    <LiquidGlassProvider style={{ position: 'relative', minHeight: '100vh' }}>
      {/* Anything here becomes the "background" that gets refracted */}
      <img src="/hero.jpg" alt="" style={{ width: '100%' }} />

      <LiquidGlass
        style={{
          position: 'absolute',
          top: 40,
          left: 40,
          width: 280,
          height: 120,
          padding: 24,
        }}
        borderRadius={24}
        refraction={0.06}
        blur={4}
        chromaticAberration={0.4}
      >
        <h2>Hello</h2>
      </LiquidGlass>
    </LiquidGlassProvider>
  );
}

That's the whole API for typical use. The provider owns one shared WebGL context; every <LiquidGlass> inside it is just a uniform-state record that draws to the same canvas.

Props

<LiquidGlassProvider>

| Prop | Type | Default | Notes | |---|---|---|---| | maxDpr | number | 2 | Caps the canvas pixel ratio. Higher = sharper, more GPU & memory. Most retina screens look fine at 2. | | autoInvalidate | boolean | true | If true, watches the DOM with a MutationObserver and recaptures on changes (debounced). | | invalidateDebounceMs | number | 120 | Debounce window for mutation-driven recapture. Mutation bursts (typical of React reconciliation) wait this long after the last change before triggering a capture. | | captureCooldownMs | number | 50 | Minimum interval between successive captures from any source. Acts as a floor that coalesces scroll, mutation, and dynamic-mode captures. Lower = more responsive scrolling, higher = less main-thread cost. | | captureScale | number (0.25–1) | 1 | Texture-resolution multiplier for the captured background. Drop to 0.7 for ~50% capture cost reduction; the result is invisible after blur. | | pauseWhenOffscreen | boolean | true | Pause the rAF render loop entirely when the provider scrolls out of the viewport. Recommended for long pages. | | as | ElementType | 'div' | Tag to render as. |

Imperative ref: { invalidate(): void } — call it to manually trigger a recapture.

<LiquidGlass>

All visual props are optional; sensible defaults are baked in. Refraction is modelled as three independent layers — edge (rim bend), rim (thin lip just inside the edge), and base (broad centre warp) — each with its own intensity and decay rate.

| Prop | Type | Default | Notes | |---|---|---|---| | borderRadius | number (px) | 16 | Rounded corners. A circle is borderRadius = halfMin, a pill is borderRadius = halfHeight with width > height. | | refraction | number | 0.06 | Edge layer intensity. The dominant refraction term — bend at the rim. Fraction of element size. | | edgeDistance | number | 0.04 | Decay rate of the edge layer. Higher = bend is concentrated tighter against the rim. | | rimIntensity | number | 0.025 | Rim layer intensity — a thin extra refraction just inside the edge that reads as a glassy lip. | | rimDistance | number | 0.12 | Decay rate of the rim layer. | | baseIntensity | number | 0.012 | Centre layer intensity — a soft broad bend that reads as glass thickness. | | baseDistance | number | 0.008 | Decay rate of the centre layer. | | cornerBoost | number | 0.02 | Extra refraction near rectangle corners. No visible effect on circles or pills. Set to 0 to disable. | | rippleEffect | number | 0 | Tangential rim shimmer — a subtle wavy-glass effect. Try 0.0050.02. | | blur | number (px) | 4 | Background Gaussian blur radius in pixels. Sampled in-shader as a 13-tap circular kernel; mipmap LOD is auto-selected for large values. | | chromaticAberration | number | 0.4 | RGB split along the refraction direction. Try 0.30.6 for a subtle prismatic edge. | | tint | string (CSS color) | '#ffffff' | Solid tint colour mixed in lightly over the refracted background. | | tintOpacity | number (0–1) | 0.06 | Strength of the tint composition. Drives the vertical-gradient tint, the ambient pickup, and the solid-tint mix. | | ambientTint | boolean | true | Sample wide horizontal bands of the captured background above and below the element and use them as a vertical gradient tint. This is what gives Apple's glass its "I picked up the colour of the room" feel. | | specular | number | 0.6 | Strength of the directional rim highlight + the always-on inner rim sheen. Set to 0 to disable both. | | lightDirection | [number, number] | [0.5, -0.7] | 2D direction of the implied light, in canvas space (positive Y points down). | | dynamic | boolean | false | If true, recaptures the background every frame for this element. Use only when content behind the glass actually animates. Throttled by the provider's captureCooldownMs (default 50ms = ~20 captures/sec). |

Performance

Three things drive cost, in order:

1. DOM rasterization (the big one). html-to-image is the only realistic way to get the page into a texture, and it takes 30–200ms on a typical viewport. Everything in this library is built around making it run as rarely as possible.

  • The capture is explicit by default: it only fires when a MutationObserver detects a real DOM change, when the root scrolls, when the root resizes, or when you call invalidate().
  • A unified capture cooldown (default 50ms) coalesces every source — scroll, mutation, dynamic mode — into one dial. Bursts collapse into a single capture.
  • A mutation-only debounce (default 120ms) gives React reconciliation time to settle before capturing. Scrolls don't pay this delay; they only pay the cooldown.
  • A fast path skips html-to-image entirely when the root contains a single <img>/<video>/<canvas> covering ≥95% of the area — common for hero images and video backgrounds.
  • Off-screen pause: when pauseWhenOffscreen is on (default), an IntersectionObserver halts the rAF loop entirely while the provider is scrolled out of view. Long pages pay nothing for off-screen glass sections.
  • captureScale lets you trade sharpness for speed on the rasterization pass. captureScale={0.7} cuts the texture pixel count in half; the result is invisible after blur.
  • dynamic={true} is opt-in per element, throttled by captureCooldownMs. Still a last resort — if only one small region behind the glass is animating, repaint that region into a <canvas> yourself and use the fast path.

2. Shader cost. ~1ms at typical sizes. Three-layer refraction + 13-tap Gaussian (×3 with chromatic aberration) + ambient sampler + specular fits in ~50 texture samples per pixel.

  • Blur is a 13-tap circular Gaussian sampled in-shader, with the mipmap LOD chosen by the requested sigma — large blurs keep their sample count fixed and fall back to the prefiltered mip chain for the wide support.
  • Refraction normals come from a single SDF gradient (3 SDF samples). The same SDF mathematically reduces to the analytical capsule / circle SDF when borderRadius matches the geometry, so it gives correct shape-aware normals for all three shapes without branching.
  • Ambient tint samples 24 taps at LOD 5 — independent of element size or blur strength.
  • Texture re-uploads use texSubImage2D when capture dimensions are unchanged, avoiding GPU memory reallocation.
  • Mipmap regeneration is skipped entirely if no registered element actually needs a mip chain (no blur, no ambient tint).

3. Per-element overhead. One uniform update + one drawArrays call per glass element per frame. Resize bursts on N elements are coalesced into a single root-rect read per frame. Negligible up to a few dozen elements.

Things you can do to keep it fast

  • Wrap a constrained area, not the whole page. The provider sizes its canvas to its own bounding rect. Wrapping a 4000px-tall scrolling page costs you a 4000px-tall texture, and html-to-image has to rasterize all of it. Wrap a hero section, a card grid, a modal — not the body.
  • Drop captureScale to 0.7 or 0.5 for any glass that uses real blur. The texture gets softer, but blur was already softening it. Visual difference is near-zero, capture cost drops by 2× or 4×.
  • Avoid putting <LiquidGlass> in tight loops. Every element costs an extra draw call and a ResizeObserver. A header full of glass icons is fine; a virtualized list of 200 glass rows is not.
  • Use the fast-path layout when you can. If your background is just an image, give it >95% of the root's area and the rasterization step is skipped entirely.
  • Set autoInvalidate={false} and call invalidate() yourself if you know your background only changes at specific moments. The MutationObserver is cheap but not free, and it can fire on benign style updates from CSS-in-JS.
  • Don't pass fresh lightDirection={[x, y]} array literals every render unless you actually want it animated. The component compares it element-wise so a stable value won't cause spurious re-syncs, but the value-equality check still runs.

Constraints to know about

  • Glass elements are forced to z-index: 1. The shared canvas sits at z-index: 1 too, painted first via DOM order. Page content at default z-index: auto sits below; glass children float naturally on top via isolation: isolate. Other elements with explicit z-index > 1 will sit above the canvas — set them higher if you need them above the glass too.
  • The provider's position is forced to relative if it's static. Otherwise the canvas can't position itself.
  • Glass element backgrounds are forced transparent. The whole point is for the WebGL canvas underneath to show through.
  • No SSR rendering. The provider sets up GL on mount in a useEffect, so the canvas only exists client-side. The component is marked "use client" for app-router compatibility.
  • One WebGL context per provider. Browsers cap WebGL contexts per page (~16 in Chrome, fewer in Safari). Don't nest providers; one per "scene" is the intended model.
  • Cross-origin images won't capture unless served with crossorigin="anonymous" and proper CORS headers. This is an html-to-image constraint.

Low-level API

If you don't want React, the renderer is exported separately:

import { GlassRenderer } from '@el-gladiador/liquid-glass-react/core';

const renderer = new GlassRenderer({ root: document.querySelector('#scene')! });
const handle = renderer.add(document.querySelector('#card')!, {
  borderRadius: 24,
  refraction: 0.05,
});

handle.setOptions({ blur: 3 });
handle.invalidate();
const stop = renderer.registerDynamic(document.querySelector('#card')!);
stop();
handle.destroy();
renderer.destroy();

License

MIT