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

@surdeddd/liquidglass

v0.11.0

Published

Liquid Glass for the whole web in one package: core engine plus /react, /vue, /svelte and /element entries

Downloads

462

Readme

LiquidGlassJS

Liquid Glass for the whole web — one engine, every browser, every framework.

CI npm size license

Real refraction over live DOM — not a screenshot, not Chromium-only. Living spring physics, iOS-26-style metaball merging, adaptive contrast, and accessibility baked in.

Live demo & playground → · API reference · npm

LiquidGlassJS — real refraction over live DOM

Why another liquid glass library?

Every existing implementation picked one rendering lane and died in it. LiquidGlassJS probes what the browser can do and routes each surface to the best available backend — under a single API:

@surdeddd/liquidglass
├─ physics        springs: press squash, release wobble, hover magnetism
├─ material       ior, frost, bevel, dispersion, specular, tint + presets
├─ a11y           backdrop tone sampling, reduced motion / transparency
├─ dom-sync       resize / intersection / scroll tracking
└─ backends
   ├─ css-svg         backdrop-filter + displacement maps   Chromium · live DOM
   ├─ svg-content     counter-positioned live copy          Safari + Firefox · live DOM
   ├─ webgl-scene     full GPU optics over owned scenes     explicit
   ├─ webgl-overlay   one shared canvas + page snapshot     metaball merging
   └─ css-fallback    graceful blur + tint                  everywhere

How it compares

| | LiquidGlassJS | liquid-glass-react | liquidGL | samasante | liquid-dom | | --- | --- | --- | --- | --- | --- | | Live-DOM refraction in Chromium | ✅ | ✅ | ❌ snapshot | ✅ | 🚧 behind flag | | Live-DOM refraction in Safari/Firefox | ✅ | ❌ flat blur | ❌ snapshot | ✅ manual mode | ❌ | | Automatic capability tiering | ✅ | ❌ | ❌ | ❌ | ❌ | | Physics system (press/wobble/hover) | ✅ | partial | ❌ | partial | ❌ | | Metaball merging over your page | ✅ | ❌ | ❌ | ❌ | ❌ | | Adaptive contrast (auto tint + tone hook) | ✅ | ❌ manual flag | ❌ | ❌ | ❌ | | prefers-reduced-motion / transparency | ✅ | ❌ | ❌ | ❌ | ❌ | | Frameworks | vanilla · element · react 18+19 · vue 3 · svelte | react 19 only | vanilla | react | react 19 | | Core dependencies | 0 | — | 0 | — | — |

Competitor capabilities as of July 2026 — methodology and sources in docs/research/competitive-landscape.md. The engine ships zero runtime dependencies; the optional snapshot tier bundles a vendored copy of html-to-image into a lazily imported chunk, credited in THIRD-PARTY-NOTICES.md.

Requirements

| | Supported | | --- | --- | | React | >=18 <20 | | Vue | ^3.4 | | Svelte | 4 or 5 | | Node (toolchain only) | >=20.19 | | Chromium | 76+ for the blur fallback; the css-svg tier also needs backdrop-filter: url(), the GL tiers need WebGL2 | | Safari | any version where CSS.supports('filter', 'url(#lg)') is true — the content tier is feature-detected, not version-gated | | Firefox | the same feature test; backdrop-filter: url() is not implemented there, so Firefox never resolves to css-svg |

Anything that fails those tests lands on css-fallback, which is blur and tint — never a broken surface. Per-engine detail and the fidelity matrix live in docs/browser-support.md.

What it looks like

| Spring physics | Metaball merging + tab bar | | --- | --- | | Press squash, wobbly release, magnetic hover | Lenses melting together over the live page |

| iOS showcase | Config-exporting playground | | --- | --- | | Lock screen, control center and tab bar rebuilt from library primitives | Material sliders that export an attach() config |

One package

| Entry | What you get | | --- | --- | | @surdeddd/liquidglass | Framework-agnostic engine — attach(), zero dependencies | | @surdeddd/liquidglass/element | <liquid-glass> web component, works in any framework | | @surdeddd/liquidglass/react | React 18 & 19 component + hooks | | @surdeddd/liquidglass/vue | Vue 3 component + v-liquid-glass directive | | @surdeddd/liquidglass/svelte | Svelte action |

React, Vue and Svelte are optional peers — install only the framework you already use.

Quick start

npm i @surdeddd/liquidglass

No build step — one script tag from a CDN:

The CDN build carries the web component and registers <liquid-glass> on load, so a script tag is the whole setup. Drop the version to track latest at your own risk.

<liquid-glass preset="frosted">Hello</liquid-glass>
<div data-liquid-glass-auto='{"preset":"clear"}'>glass</div>

<script src="https://unpkg.com/@surdeddd/[email protected]/dist/liquidglass.global.js"></script>
<script>
  LiquidGlass.autoAttach()
</script>
import { attach } from '@surdeddd/liquidglass'

const panel = document.querySelector<HTMLElement>('.panel')
if (panel) {
  const glass = attach(panel, {
    preset: 'frosted',
    ior: 1.5,
    dispersion: 0.3,
    motionLight: true,
    physics: { wobble: 0.8 }
  })

  glass.on('tonechange', tone => console.log('backdrop is', tone))
  glass.set({ preset: 'clear' })
  glass.destroy()
}
<script type="module">
  import { define } from '@surdeddd/liquidglass/element'
  define()
</script>

<liquid-glass preset="frosted" merge="dock">Hello</liquid-glass>
import { LiquidGlass } from '@surdeddd/liquidglass/react'

<LiquidGlass as="nav" preset="clear" dispersion={0.3}>…</LiquidGlass>
<script setup>
import { LiquidGlass, vLiquidGlass } from '@surdeddd/liquidglass/vue'
</script>

<template>
  <LiquidGlass preset="frosted" :options="{ dispersion: 0.3 }">…</LiquidGlass>
  <div v-liquid-glass="{ preset: 'clear' }">…</div>
</template>
<script>
  import { liquidGlass, glass } from '@surdeddd/liquidglass/svelte'
</script>

<div use:liquidGlass={{ preset: 'frosted' }}>…</div>

<!-- svelte 5 attachment -->
<div {@attach glass({ preset: 'frosted' })}>…</div>

The Vue directive is registered locally by importing vLiquidGlass into <script setup>; register it globally with app.directive('liquid-glass', vLiquidGlass) if you prefer.

Each entry has a guide of its own, covering how options map to that framework's idioms, the escape hatch back to the engine handle, server rendering and cleanup: React · Vue · Svelte · web component.

On Safari and Firefox the engine refracts a designated element rather than the whole page. It picks the nearest ancestor that paints a background on its own; pass backdrop when you want a specific one — it is also the cheaper choice, because that element gets cloned into the refraction layer.

attach(panel, { backdrop: '.hero-art' })

Options

Every option is optional and can be changed at runtime through set(). Numeric values are clamped to the range shown; anything non-finite falls back to the default. Where a preset moves a value the default column reads clear / frosted / tintedclear is what you get when you pass no preset.

| Option | Type | Default | Range | Notes | | --- | --- | --- | --- | --- | | preset | 'clear' \| 'frosted' \| 'tinted' | 'clear' | — | Starting point for every material value below | | blur | number | 2 / 10 / 8 | 0–100 | Backdrop blur in px | | saturation | number | 1.4 / 1.6 / 1.4 | 0–3 | Backdrop saturation multiplier | | brightness | number | 1 / 1.05 / 1 | 0–3 | Backdrop brightness multiplier | | tint | string | #ffffff, #7c5cff under tinted | hex or rgb() | Set it explicitly to opt out of adaptive tinting | | tintOpacity | number | 0.06 / 0.14 / 0.28 | 0–1 | Tint alpha | | refraction | number | 0.65 / 0.45 / 0.5 | 0–1 | Strength of the rim bend | | ior | number | 1.5 | 1–2.5 | Index of refraction; 1 bends nothing | | magnify | number | 0.02 / 0.015 / 0.015 | 0–0.1 | Whole-body magnification | | thickness | number | 'auto' | 'auto' | 0–100 | Glass depth in px | | bevelWidth | number | 'auto' | 'auto' | 0–200 | Rim band width; auto tracks the corner radius | | bevelDepth | number | 0.6 | 0–1 | Rim profile curvature | | dispersion | number | 0.15 | 0–1 | Chromatic split at the rim — Chromium and WebGL tiers only | | specular | number | 0.6 | 0–1 | Bezel highlight strength; 0 removes the bezel layer | | shadow | number | 0.55 | 0–1 | Cast shadow under the glass — a soft ambient sized from the element plus a contact line; 0 removes it. Painted on every tier | | frost | number | 0 / 0.35 / 0 | 0–1 | Grain displacement | | radius | number | 'auto' | 'auto' | ≥ 0 | auto reads the element's border-radius | | shape | 'rounded' \| 'squircle' | 'rounded' | — | Squircle also clips the host | | backend | BackendId \| 'auto' | 'auto' | — | Honoured only if the tier is supported | | backdrop | Element \| string \| null | null | — | Refraction source for svg-content | | sceneImage | string | null | null | — | Texture for webgl-scene | | physics | boolean | { press, hover, wobble } | truepress and hover on, wobble 0.6 | wobble 0–1 | Disabled entirely under reduced motion; hover turns itself off on coarse pointers unless you pass it explicitly | | merge | string | null | null | — | Metaball group name; needs webgl-overlay, and a group holds at most 8 lenses | | mergeStrength | number | 30 | px, unclamped | Distance at which group members melt together; <liquid-glass-group spacing> sets it to 40 | | adaptive | boolean | true | — | Backdrop tone sampling and automatic tint flip | | motionLight | boolean | false | — | Drive the bezel highlight from device orientation | | quality | { mapSide: number, caPasses: 1 \| 3, maxDpr: number } | device tier | — | Per-surface override of the quality profile; layers on top of configure() |

A merge group is capped at 8 lenses, which is the size of the shader's shape array. The ninth and later members of a group are dropped from the overlay pass entirely — they keep their blur, tint and shadow but get neither refraction nor merging, and nothing is logged. Split a longer dock into a second merge group.

Runtime and events

const glass = attach(el, { preset: 'frosted' })

glass.on('backendchange', id => console.log('now rendering with', id))
glass.on('tonechange', tone => root.classList.toggle('on-light', tone === 'light'))
glass.on('press', point => console.log('pressed at', point.x, point.y))
glass.on('release', () => {})
glass.on('degrade', id => console.log('fps watchdog dropped to', id))

glass.options.preset

Every payload is typed per event: backendchange and degrade give a BackendId, tonechange gives 'light' | 'dark' | null, press gives the point relative to the element's own top-left corner rather than the viewport, release gives null. handle.options reports the resolved configuration, and every subscription returns its own unsubscribe function.

The web component mirrors the same events onto the DOM as composed liquid-glass:* CustomEvents, so a page without a handle can listen too:

document.addEventListener('liquid-glass:tonechange', event => console.log(event.detail))

One glass per element. attach() on an element that already has one returns the same handle and applies the new options — it is attach-or-update, not a second surface. destroy() and detach() remove it for everyone, so two independent owners on the same node share a lifetime; give them separate elements if they need separate lifetimes. Calling destroy() twice is safe, and a stale handle cannot tear down a surface that was attached after it.

Beyond the handle:

| API | What it does | | --- | --- | | autoAttach(root?) | Attaches every [data-liquid-glass-auto] element and keeps watching for new ones. Returns a stop function; inert without a DOM | | configure({ mapSide, caPasses, maxDpr, snapshotThrottleMs, overlayZIndex }) | Overrides the quality profile the device tier picked | | resetQuality() | Clears every override, including the caPasses: 1 the fps watchdog writes | | deviceTier() / getQuality() | Reads what the engine decided for this device | | probeCapabilities() | The capability snapshot behind backend selection | | mountScrollEdge(el, { position = 'top', size = 96, strength = 12 }) | Progressive blur edge for floating bars; returns a handle whose destroy() you have to call | | morphGlass(from, to, { stiffness = 320, damping = 26 }) | Hands one control's geometry to another on a spring; returns a promise that resolves when the spring settles | | getInstance(el) / detach(el) | Reach or tear down a surface you did not keep a handle to | | VERSION | The version string this build was stamped with |

The resolved state is also on the element, which makes it inspectable in devtools: data-liquid-glass (preset), data-liquid-glass-backend, data-liquid-glass-tone, data-liquid-glass-pressed, data-liquid-glass-degraded. A morphing element also carries data-liquid-glass-morphing for the length of the spring.

What attach() takes over

The engine paints through inline styles on your element, so some properties stop being yours for the lifetime of the glass. Whatever you had set inline is captured at mount and put back by destroy(), but while the surface is attached the engine's value wins. backdrop-filter below stands for both the prefixed and the unprefixed form, and border-radius is only overwritten when radius is a number or a squircle needs one — at radius: 'auto' the engine reads yours instead.

| Tier | Inline properties it owns | | --- | --- | | css-svg | backdrop-filter, background, border-radius, box-shadow, clip-path | | svg-content | the same, plus isolation and position | | webgl-overlay | backdrop-filter, background, border-radius, box-shadow, position, z-index | | webgl-scene | background, border-radius, isolation, position — this is the one tier that paints no cast shadow | | css-fallback | background, backdrop-filter, border-radius, box-shadow, clip-path |

Two consequences are worth planning for. Your own background and box-shadow are replaced — the glass is the surface now, and shadow: 0 is how you ask for no cast shadow. And a host whose computed position is static becomes relative, which re-parents its absolutely positioned children; the specular bezel and the press glow do that on every tier, since both mount a layer inside the host. The division that works: style layout on the host — size, margin, padding, and the border-radius that radius: 'auto' reads — and leave paint to the engine.

Highlights

  • Real lens optics — a convex squircle dome refracted by Snell's law (ior, default 1.5): optically flat interior with a subtle whole-body magnification (magnify) and all the bending concentrated in a rim band that tracks your corner radius, exactly like iOS 26.
  • Edge chromatic aberrationdispersion splits R/G/B along the rim on the Chromium and WebGL paths (fidelity matrix).
  • Living specular bezel — a two-tone rim highlight that follows the pointer (or device tilt with motionLight: true) instead of a painted-on gradient.
  • Tiered rendering — capability probe picks the best backend per browser; fidelity improves as browsers ship new APIs, your code never changes.
  • Metaballs — wrap lenses in <liquid-glass-group spacing="48"> (or share a merge group) and they melt into each other through an SDF smooth-min shader, the GlassEffectContainer way. Up to 8 lenses per group.
  • Scroll edgemountScrollEdge(document.body, { position: 'top' }) progressively dissolves content under your floating bars, like iOS scroll edge effects.
  • MorphingmorphGlass(from, to) hands one control's geometry to another on a spring, the glassEffectID transition.
  • Living physics — a mass–spring–damper system drives gel squash, wobbly release and magnetic hover on any backend; sleeps when idle.
  • Adaptive contrast — glass samples backdrop luminance and follows it the way Apple's material does: dark smoke over dark content, white frost over light. It exposes data-liquid-glass-tone and a ready --lg-on-glass color for your text.
  • Accessible by default — reduced motion and reduced transparency are respected live; every injected layer is aria-hidden. Reduced transparency is only observable from a page in Chromium 118+, so the opaque fallback is a bonus rather than a contrast plan.
  • Fast enough to be honest about — ten lenses scrolling continuously start in the mid-30s with dispersion at full quality and settle around 100 once the fps watchdog drops the extra passes; render-on-demand everywhere, and nothing keeps a frame loop alive when the page is still. Numbers from the included bench, not a guess.

Troubleshooting

Safari or Firefox shows flat blur. Those engines refract a cloned source rather than the live backdrop. The engine falls back to the nearest ancestor that paints a background — if every ancestor is transparent, nothing can be cloned. Pass backdrop explicitly.

merge does nothing. Metaball merging exists only on webgl-overlay. Under backend: 'auto' the engine now switches to it when a merge group is set and WebGL2 is available; if WebGL2 is missing, the group is dropped and the engine logs it once. <liquid-glass-group> sets the backend for its children.

Strict CSP blocks the worker. Lens maps are generated in a worker spawned from a Blob URL, so allow worker-src blob:. Without it the library silently generates maps on the main thread — same output, more main-thread work.

Next.js / Nuxt / SvelteKit. Every entry imports cleanly on the server and attach() is a client-side call; run it from an effect. autoAttach() is safe to call anywhere — it returns an inert stop function when there is no DOM.

React Server Components. @surdeddd/liquidglass/react ships without a "use client" banner, so importing it from a server component fails. Put 'use client' at the top of your own file — the one that imports LiquidGlass, useLiquidGlass or useLiquidGlassHandle — or reach it through dynamic(() => import('./glass'), { ssr: false }). pnpm ssr only proves the entries import in bare Node; it does not exercise the RSC boundary.

Only some of my lenses merge. A merge group holds 8. The ninth and later members are dropped from the overlay pass — blur, tint and shadow survive, refraction and merging do not, and nothing is logged. Split the rest into a second group.

Images vanish inside a metaball group. The webgl-overlay tier rasterizes the page to a texture, so cross-origin images without CORS headers and webfonts that cannot be inlined do not make it into the snapshot.

Reduced transparency or Windows High Contrast. The surface switches to an opaque material (refraction and dispersion off, tint raised). This is deliberate, and it follows the OS setting live.

Text over glass is unreadable. adaptive samples the backdrop and matches the default material to it — smoke over dark, frost over light — but it cannot resolve a luminance over a raster image backdrop. Use color: var(--lg-on-glass), the tonechange event or the data-liquid-glass-tone attribute to style text yourself, or set tint explicitly.

More symptoms, and more depth on these, in docs/troubleshooting.md — stacking order under a merge group, the two CSP directives, hydration, and how to get off a watchdog demotion.

Stability

Pre-1.0. A minor bump can carry a breaking change, and the release notes say so when one does.

The public API is what @surdeddd/liquidglass and its /element, /react, /vue and /svelte entries export. Within that:

| | | | --- | --- | | Stable in a minor | attach, detach, getInstance, autoAttach, the LiquidGlassHandle shape, the five event names and their payloads, the option names in the table above, the data-liquid-glass* attributes, the <liquid-glass> attribute surface | | May change in a minor | Rendered output — optics, defaults, the values a preset resolves to. Which backend auto picks on a given engine. The clamp ranges. Anything a new browser API makes better | | Not public at all | Everything exported for the demo and the tests — GlRenderer, sdfSuperellipse, setLuminanceGrid, MAX_SHAPES, the backends/* objects. These can change in a patch |

The five workspace packages (-core, -element, -react, -vue, -svelte) are private build inputs versioned in lockstep with the published package; only @surdeddd/liquidglass is installable. Release history is packages/liquidglass/CHANGELOG.md.

Development

pnpm install
pnpm build && pnpm typecheck && pnpm lint && pnpm test && pnpm coverage && pnpm ssr && pnpm size
pnpm e2e

That is the same list CI runs, in the same order. pnpm build comes first because the framework adapters resolve the engine through its built output, so their tests run against packages/core/dist rather than src.

The fps benchmark needs the demo served on 4173, which is the address pnpm bench opens — vite alone serves 5173 and the bench would find nothing there:

pnpm --filter demo exec vite --port 4173 --strictPort   # one shell
pnpm bench                                              # another; exits non-zero below 55 fps

BENCH_URL overrides the address: BENCH_URL=http://localhost:5173/bench.html pnpm bench if the demo is already up on the vite default. pnpm bench is headless; the numbers below come from node scripts/fps-bench.mjs --headed.

The bench reports two numbers, because one would be misleading. Ten lenses scrolling on headed Chromium on an M-series machine open in the mid-30s — every lens running three displacement passes for chromatic dispersion — and settle around 100 fps once the fps watchdog decides the page cannot afford them and drops to one. The same page with no glass on it runs at ~118 fps, so the lenses are the cost, and dispersion is most of it.

Headless lands far lower on both counts: it renders through SwiftShader, which is a software rasterizer figure rather than a rendering regression.

The landing + playground lives in apps/docs, the test harness in apps/demo, research notes in docs/research.

Documentation

docs/ is the index — what each document is for and the order to read them in.

| | | | --- | --- | | React · Vue · Svelte · Web component | One guide per entry: options as idioms, the handle escape hatch, SSR, cleanup, a composed example | | Recipes | Whole components: nav bar, tactile card, melting tab bar, morph, lens over your own art | | Accessibility | What the engine handles, what stays yours, and how to check either | | Troubleshooting | Symptom first: flat lenses, merge groups, stacking order, CSP, hydration, a stuck watchdog | | Performance | What each surface costs, which knob moves it, how the fps watchdog behaves | | Browser support | Engine-by-engine behaviour, the per-backend fidelity matrix, version floors | | Architecture | Module map, dependency direction, how tiers are selected | | API reference | Generated from the published entry points |

License

MIT