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

@elemeta/glasswave

v0.7.2

Published

GPU-accelerated liquid glass and morphing surfaces for Vue, React, and Next.js, powered by WebGL2

Readme

Glasswave

GPU-accelerated liquid glass and morphing surfaces for Vue, React, and Next.js, powered by WebGL2.

Glasswave renders every glass surface inside a LiquidBackdrop through one shared GPU scene. Your content stays in the DOM, while refraction, depth, dispersion, frost, and optional SDF morphing are rendered against the backdrop.

Highlights

  • One shared WebGL2 canvas per backdrop, with an instanced fast path and ordered composition only for overlapping lenses.
  • Typed Vue, React, and Next.js adapters with the same material contract.
  • Dynamic URL, image, video, canvas, and ImageBitmap backdrop sources with frame-aware texture uploads.
  • Smooth SDF morphing between nearby glass surfaces.
  • Cached geometry, viewport culling, dirty-state scheduling, and event-driven rendering.
  • DOM content, focus, pointer input, and accessibility remain outside the canvas.
  • Runtime metrics and context-loss lifecycle events for production diagnostics.

Install

npm install @elemeta/glasswave

Or with Bun:

bun add @elemeta/glasswave

Install the peer dependencies for the framework used by your application:

# Vue
npm install vue

# React or Next.js
npm install react react-dom

Supported peers:

  • Vue 3.5 or newer
  • React 18.3 or React 19
  • Next.js through the React client entry

How it works

Glasswave uses two primary primitives:

  • LiquidBackdrop owns the background image or color and one shared WebGL2 canvas.
  • LiquidGlass registers one lens inside that backdrop.
LiquidBackdrop
|-- backdrop image or color
|-- shared WebGL2 canvas
|-- LiquidGlass
|-- LiquidGlass
`-- LiquidGlass

Use one backdrop for all lenses that sample the same visual source. Do not create a separate backdrop for every button.

Material API

Every lens accepts a typed glassEffect object with seven primary controls:

import type { LiquidGlassEffect } from '@elemeta/glasswave'

const effect: LiquidGlassEffect = {
  lightAngle: -45,
  lightIntensity: 0.3,
  refraction: 80,
  depth: 20,
  dispersion: 0.5,
  frost: 4,
  splay: 0,
}

| Parameter | Range or unit | Description | | --- | --- | --- | | lightAngle | degrees | Direction of the edge light. | | lightIntensity | 0...1 | Strength of the edge light. 0.3 means 30%. | | refraction | 0...100 | Edge-refraction strength. 80 means 80%. | | depth | CSS pixels, >= 0 | Width of the curved refracting band inside the edge. | | dispersion | 0...1 | Separation of red, green, and blue light. | | frost | CSS pixels, >= 0 | Blur applied to the sampled backdrop. | | splay | 0...1 | Angular spread of the directional highlight. |

lightIntensity: 0.3 controls the edge light, not the opacity of the complete lens. refraction: 80 uses Glasswave's 0...100 optical control scale. Use color and opacity for the material body.

The values above are a balanced starting profile. Every field is optional.

Recommended tuning order

  1. Match lightAngle to the scene.
  2. Set lightIntensity for edge visibility.
  3. Increase refraction until the bend is readable.
  4. Adjust depth until the refracting edge is proportional to the lens.
  5. Add frost only when softness is needed.
  6. Add restrained dispersion if chromatic separation benefits the scene.
  7. Finish with splay.

Vue

<script setup lang="ts">
import type { LiquidGlassEffect } from '@elemeta/glasswave'
import { LiquidBackdrop, LiquidGlass } from '@elemeta/glasswave/vue'
import '@elemeta/glasswave/style.css'

const effect: LiquidGlassEffect = {
  lightAngle: -45,
  lightIntensity: 0.3,
  refraction: 80,
  depth: 20,
  dispersion: 0.2,
  frost: 4,
  splay: 0,
}
</script>

<template>
  <LiquidBackdrop
    class="glass-scene"
    src="/wallpaper.webp"
    alt=""
    fit="cover"
    position="50% 50%"
  >
    <LiquidGlass
      class="glass-panel"
      :glass-effect="effect"
      :radius="36"
      color="#1A1A26"
      :opacity="0.5"
    >
      Liquid glass
    </LiquidGlass>
  </LiquidBackdrop>
</template>

<style scoped>
.glass-scene {
  position: relative;
  width: min(720px, 100%);
  min-height: 420px;
  overflow: hidden;
}

.glass-panel {
  position: absolute;
  top: 50%;
  left: 50%;
  display: grid;
  width: 320px;
  min-height: 120px;
  place-items: center;
  transform: translate(-50%, -50%);
}
</style>

React

import type { LiquidGlassEffect } from '@elemeta/glasswave'
import { LiquidBackdrop, LiquidGlass } from '@elemeta/glasswave/react'
import '@elemeta/glasswave/style.css'
import './glass-example.css'

const effect: LiquidGlassEffect = {
  lightAngle: -45,
  lightIntensity: 0.3,
  refraction: 80,
  depth: 20,
  dispersion: 0.2,
  frost: 4,
  splay: 0,
}

export function GlassExample() {
  return (
    <LiquidBackdrop
      className="glass-scene"
      src="/wallpaper.webp"
      alt=""
      fit="cover"
      position="50% 50%"
    >
      <LiquidGlass
        className="glass-panel"
        glassEffect={effect}
        radius={36}
        color="#1A1A26"
        opacity={0.5}
      >
        Liquid glass
      </LiquidGlass>
    </LiquidBackdrop>
  )
}
.glass-scene {
  position: relative;
  width: min(720px, 100%);
  min-height: 420px;
  overflow: hidden;
}

.glass-panel {
  position: absolute;
  top: 50%;
  left: 50%;
  display: grid;
  width: 320px;
  min-height: 120px;
  place-items: center;
  transform: translate(-50%, -50%);
}

Next.js App Router

Import the client components from @elemeta/glasswave/next.

'use client'

import type { LiquidGlassEffect } from '@elemeta/glasswave'
import { LiquidBackdrop, LiquidGlass } from '@elemeta/glasswave/next'

const effect: LiquidGlassEffect = {
  lightAngle: -45,
  lightIntensity: 0.3,
  refraction: 80,
  depth: 20,
  dispersion: 0.2,
  frost: 4,
  splay: 0,
}

export function GlassCard() {
  return (
    <LiquidBackdrop className="glass-scene" src="/wallpaper.webp" alt="">
      <LiquidGlass
        className="glass-panel"
        color="#1A1A26"
        opacity={0.5}
        glassEffect={effect}
        radius={36}
      >
        Liquid glass
      </LiquidGlass>
    </LiquidBackdrop>
  )
}

Load the shared stylesheet from the root layout or the application's global stylesheet:

import '@elemeta/glasswave/style.css'

WebGL initialization and geometry measurement begin after mount, so package imports remain SSR-safe.

Multiple lenses

All descendant lenses share the backdrop canvas. Non-overlapping lenses use the instanced fast path; intersecting lenses are composited in layer order:

<LiquidBackdrop class="scene" src="/wallpaper.webp" alt="">
  <LiquidGlass class="card card-a" :glass-effect="effect">A</LiquidGlass>
  <LiquidGlass class="card card-b" :glass-effect="effect">B</LiquidGlass>
  <LiquidGlass class="card card-c" :glass-effect="effect">C</LiquidGlass>
</LiquidBackdrop>

Create another LiquidBackdrop only when a region requires a different source image, color, fit, position, or independent coordinate system.

Surface material and overlapping layers

Use color and opacity for the body of the material. Glasswave renders one cross-platform GPU surface highlight; ordinary, layered, and morphing lenses use the same edge-light shader while WebGL-unavailable environments retain the DOM fallback. highlightStyle accepts only surface.

<LiquidGlass
  color="#1A1A26"
  opacity={0.5}
  radius={36}
  layer={2}
  glassEffect={effect}
>
  Content
</LiquidGlass>

When lenses overlap inside the same LiquidBackdrop, the upper lens samples the completed lower material. layer controls GPU order and the default DOM z-index. Pulling the lenses apart returns them to the instanced fast path.

Do not add an opaque CSS background, backdrop-filter, or root CSS opacity to LiquidGlass; those declarations cover or bypass the GPU material. Your CSS should define size, placement, and content layout only.

See Material and layering for complete React, Vue, Next.js, theme, migration, and glass-over-glass examples.

Dynamic backdrop sources

Use the source prop when the backdrop can change at runtime. It accepts a URL string, HTMLImageElement, HTMLVideoElement, HTMLCanvasElement, or ImageBitmap and takes precedence over the legacy src prop.

import type { GlasswaveBackdropSource } from '@elemeta/glasswave'

React and Next.js:

<LiquidBackdrop source={activeSource} alt="" fit="cover">
  <LiquidGlass className="glass" glassEffect={effect} />
</LiquidBackdrop>

Vue:

<LiquidBackdrop :source="activeSource" alt="" fit="cover">
  <LiquidGlass class="glass" :glass-effect="effect" />
</LiquidBackdrop>

Source update behavior:

  • URL strings and HTMLImageElement sources upload after decoded pixels are ready.
  • HTMLVideoElement uses requestVideoFrameCallback when available, so the GPU texture changes only for newly decoded video frames.
  • HTMLCanvasElement is mutable. Increment sourceVersion after drawing a new frame.
  • ImageBitmap is immutable. Pass a new bitmap object when its pixels change. Glasswave normalizes its orientation before upload so it matches image, video, and canvas sources.
  • Transparent sources use straight-alpha upload and explicit premultiplied composition, avoiding colored fringes around translucent PNG or canvas pixels.
  • Rapid URL or element replacement is generation-safe: superseded loads are aborted and cannot overwrite a newer source.
  • A failed replacement reports onError / @error but keeps the last valid frame and active scene instead of flashing or clearing the glass.
  • Decoded video-frame uploads pause while the backdrop is outside the viewport, the page is hidden, or WebGL is unavailable, then resume with the latest frame.
  • Pending loads, media listeners, and frame callbacks are removed on source replacement, unmount, and context loss.
  • Scroll, resize, browser zoom, visualViewport changes, and phone orientation changes keep the visible source and optical scene aligned.
const [sourceVersion, setSourceVersion] = useState(0)

function paint() {
  drawNextFrame(canvas)
  setSourceVersion(version => version + 1)
}

<LiquidBackdrop source={canvas} sourceVersion={sourceVersion}>
  <LiquidGlass />
</LiquidBackdrop>

For video, set muted and playsInline when autoplay is required on mobile. Cross-origin image and video sources must allow CORS before their pixels can be uploaded to WebGL2. Source failures are reported through Vue @error or React/Next.js onError. Glasswave owns only its loaders and GPU upload callbacks; your application still owns playback, object URLs, streams, and ImageBitmap.close().

See Dynamic backdrop sources for lifecycle, carousel, canvas, video, CORS, and cleanup examples.

Core API

LiquidBackdrop

| Prop | Type | Default | Purpose | | --- | --- | --- | --- | | as | element type | 'div' | Root element. | | source | string \| HTMLImageElement \| HTMLVideoElement \| HTMLCanvasElement \| ImageBitmap | unset | Dynamic visual source. Takes precedence over src. | | sourceVersion | string \| number | 0 | Explicit frame version for a mutable canvas or another manually updated source. | | src | string | '' | Image sampled by descendant lenses. | | color | string | 'transparent' | Background color and color-only texture source. | | alt | string | '' | Alternative text for the internal image. | | fit | 'contain' \| 'cover' \| 'fill' \| 'none' \| 'scale-down' | 'cover' | Image fitting and optical alignment. | | position | string | '50% 50%' | Image position. | | width, height | number | unset | Intrinsic image dimensions, not CSS layout dimensions. | | gpuMaxDpr | number | 3 | Maximum device-pixel ratio of the GPU canvas. | | gpuMaxPixels | number | 48_000_000 | Approximate total pixel budget for the uploaded source and scene render targets. Internal DPR is reduced automatically when required. | | morphController | GlasswaveMorphController | unset | Framework-neutral manual relationship controller. | | debugMorph | boolean | false | Development overlay for ids, gaps, and active links. |

LiquidBackdrop also reports scene lifecycle and diagnostics:

| Event / callback | Payload | Purpose | | --- | --- | --- | | Vue @ready / React onReady | GlasswaveSceneMetrics | First GPU frame is ready. | | Vue @metrics / React onMetrics | GlasswaveSceneMetrics | Updated runtime measurements after rendered work. | | Vue @error / React onError | Error | Initialization, texture upload, or lens snapshot failed. | | Vue @contextlost / React onContextLost | Error | The browser lost the WebGL2 context. | | Vue @contextrestored / React onContextRestored | none | The browser restored the context and Glasswave is rebuilding it. |

interface GlasswaveSceneMetrics {
  backend: 'webgl2'
  fps: number
  gpuFrameMs: number | null
  instances: number
  visibleInstances: number
  drawCalls: number
  estimatedGpuMemoryBytes: number
  dpr: number
  reason: GlasswaveRenderReason
}

gpuFrameMs is null when EXT_disjoint_timer_query_webgl2 is unavailable. GPU memory is an estimate of buffers and textures allocated by Glasswave, because browsers do not expose total driver memory usage. Glasswave also respects MAX_TEXTURE_SIZE, MAX_RENDERBUFFER_SIZE, and MAX_VIEWPORT_DIMS. A large or very long backdrop keeps its CSS dimensions but may render at an internal DPR below 1 instead of allocating an unsafe texture.

Size the backdrop with CSS. The width and height props describe intrinsic image dimensions.

LiquidGlass

| Prop | Type | Default | Purpose | | --- | --- | --- | --- | | as | element type | 'div' | Root element. | | glassEffect | LiquidGlassEffect | internal recipe | Seven primary material parameters. | | radius | number \| readonly number[] | 24 | One radius or four corner radii. | | color | string | 'transparent' | Color mixed into the refracted source. | | opacity | number | 0.75 | Material-color opacity, clamped to 0...1. | | layer | number | computed CSS order | GPU and default DOM stacking order. | | highlight | string | white | Surface edge-light color. | | highlightWidth | number | 0.5 | Highlight width in CSS pixels. | | highlightBlur | number | 0.25 | Highlight blur in CSS pixels. | | highlightStyle | 'surface' | 'surface' | The single cross-platform highlight model. | | outerShadow | string | built in | CSS shadow outside the lens. | | innerShadow | string | 'none' | CSS shadow inside the lens. | | interactive | boolean | false | Reserved compatibility flag; the base material remains static. | | morphGroup | string \| number | unset | Joins nearby lenses that share the same group. | | morphId | string \| number | unset | Stable identity for exact or manual relationships. | | morphWith | readonly (string \| number)[] | unset | Exact allow-list for this member. | | morphMode | 'auto' \| 'linked' \| 'manual' | 'auto' | Relationship source used by the member. | | morphDistance | number | 24 | Maximum edge-to-edge gap, in CSS pixels, at which morphing starts. | | morphHysteresis | number | 4 | Additional release gap for an active link. | | morphSoftness | number | 48 | Smooth-union width in CSS pixels. | | morphMaxMembers | number | 2 | Maximum members in one joined GPU surface, clamped to 2...4. |

Legacy tint, tintOpacity, and surfaceColor props remain accepted for migration. New code should use color and opacity; do not mix both contracts on one lens.

A four-corner radius follows this order:

const radius = [topLeft, topRight, bottomRight, bottomLeft]

Morph API

Choose one relationship mode. All three use the same GPU material and distance calculation.

| Goal | Required API | | --- | --- | | Any nearby members may join | morphGroup only; default morphMode="auto" | | Only named members may join | morphId, morphMode="linked", and morphWith | | Application code decides the graph | morphMode="manual" and createMorphController() |

Automatic

Use this when every nearby member in the group may join:

<LiquidGlass morphGroup="toolbar" morphMaxMembers={3} />
<LiquidGlass morphGroup="toolbar" morphMaxMembers={3} />
<LiquidGlass morphGroup="toolbar" morphMaxMembers={3} />

Exact relationships

Use stable ids and define the permitted peers. The recommended exact setup is symmetric:

<LiquidGlass morphGroup="toolbar" morphId="a" morphMode="linked" morphWith={['b', 'c']} />
<LiquidGlass morphGroup="toolbar" morphId="b" morphMode="linked" morphWith={['a']} />
<LiquidGlass morphGroup="toolbar" morphId="c" morphMode="linked" morphWith={['a']} />
<LiquidGlass morphGroup="toolbar" morphId="d" morphMode="linked" morphWith={[]} />

This permits A-B and A-C. It denies B-C and every relationship with D. A denied pair cannot enter the same multi-member surface through another member.

Manual relationships

Create one controller, pass it to the backdrop, and mark controlled members as manual:

import { createMorphController } from '@elemeta/glasswave'
import { LiquidBackdrop, LiquidGlass } from '@elemeta/glasswave/react'

const morph = createMorphController()
morph.connectMorph('toolbar', ['a', 'b', 'c'])

export function Toolbar() {
  return (
    <LiquidBackdrop src="/wallpaper.webp" morphController={morph}>
      <LiquidGlass morphGroup="toolbar" morphId="a" morphMode="manual" morphMaxMembers={3} />
      <LiquidGlass morphGroup="toolbar" morphId="b" morphMode="manual" morphMaxMembers={3} />
      <LiquidGlass morphGroup="toolbar" morphId="c" morphMode="manual" morphMaxMembers={3} />
    </LiquidBackdrop>
  )
}

connectMorph('toolbar', ['a', 'b', 'c']) creates all pairwise links between A, B, and C. Distance still decides when the visible bridge appears.

The controller has four public methods:

morph.connectMorph(group, ids)
morph.disconnectMorph(group, ids)
morph.clearMorph(group?)
morph.getMorphState(group?)

Vue uses the same controller with :morph-controller="morph" and kebab-case component props.

Stability, events, and diagnostics

  • morphDistance alone controls the activation gap; changing morphSoftness does not silently shrink it.
  • morphHysteresis adds release distance without changing bridge thickness.
  • morphSoftness controls bridge width and viscosity.
  • morphMaxMembers limits one surface to 2, 3, or 4 members.
  • Vue emits morphstart, morphchange, and morphend.
  • React and Next.js expose onMorphStart, onMorphChange, and onMorphEnd.
  • debugMorph on LiquidBackdrop shows ids, allowed links, contour gaps, and active links during development.
  • Without WebGL2, semantic DOM content remains usable but the bridge is unavailable.

See Morphing surfaces for controller state, event payloads, dynamic mounting, selection rules, and troubleshooting. Normal mounting, resizing, scrolling, pointer activity, CSS transitions, and animations invalidate the scene automatically. Vue template refs and React refs expose:

interface LiquidGlassHandle {
  readonly el: HTMLElement | null
  updateRefraction(): void
  updateBackdrop(): void
}

Use these update methods only after geometry or source transforms that browser observers cannot detect.

Images and CORS

Same-origin image and video sources work without additional configuration.

A cross-origin image or video must return a valid Access-Control-Allow-Origin header. Without it, the browser may display the media but WebGL and the synchronized presentation canvas cannot read its pixels.

For decorative images, use alt="".

For an element source, set crossOrigin = 'anonymous' before assigning src. Glasswave URL strings do this automatically.

Canvas sources are same-origin unless application code has drawn cross-origin pixels into them. A tainted canvas cannot be uploaded to WebGL. Glasswave reports an actionable error and sets the backdrop status to failed.

Rendering and performance

  • One LiquidBackdrop creates one WebGL2 canvas.
  • Non-overlapping descendant lenses use one instanced draw call; only overlapping sets require ordered compositor passes.
  • Lenses outside the backdrop, viewport, or an ancestor clipping region are removed before GPU upload.
  • Lens rectangles and material snapshots are cached; scroll, resize, material, backdrop, and transform changes invalidate only the required state.
  • Static scenes render on demand rather than running a permanent animation loop.
  • Frost textures are prepared once per backdrop, not through one CSS filter per lens.
  • dispersion: 0 uses the least expensive optical path.
  • gpuMaxDpr limits canvas resolution and defaults to 3.
  • Fifty lenses under one backdrop are substantially cheaper than fifty separate backdrops.
  • Refraction is local edge lensing; Glasswave does not uniformly zoom the complete source image.

For dense mobile interfaces, reduce gpuMaxDpr before changing the material design:

<LiquidBackdrop :gpu-max-dpr="2" />

Browser behavior

Glasswave requires WebGL2 for optical refraction and targets current evergreen browsers:

  • Chromium-based desktop and Android browsers
  • current Safari on iPhone, iPad, and macOS
  • Firefox with WebGL2 enabled

If WebGL2 initialization or texture upload fails, semantic DOM content, the surface fallback, shadows, focus, and interaction remain available. Optical refraction and ordered glass-over-glass composition are unavailable; Glasswave does not start a second renderer.

Small platform differences can still occur because GPU drivers, display density, antialiasing, color management, and browser compositing are not identical.

Troubleshooting

The effect looks like blur only

Use a detailed backdrop and lower frost. A soft or flat background can hide displacement. Then increase refraction gradually.

Refraction is missing

Check that:

  1. LiquidGlass is a descendant of LiquidBackdrop.
  2. The backdrop and lens have non-zero CSS dimensions.
  3. The backdrop has an image or non-transparent color.
  4. The image request succeeds.
  5. Cross-origin images return a valid CORS header.
  6. WebGL2 is enabled on the device.

The image looks pixelated

Use a source image with enough intrinsic resolution. If performance allows it, increase gpuMaxDpr up to the default cap of 3.

The image is optically misaligned

Keep the visible image and GPU source under the same LiquidBackdrop. Use the backdrop's fit and position props instead of independently styling another image copy.

Package entry points

| Import | Contains | | --- | --- | | @elemeta/glasswave | Shared types, material resolver, effect recipes, and framework-neutral exports. | | @elemeta/glasswave/core | Explicit framework-neutral entry. | | @elemeta/glasswave/vue | Vue components and plugin registration. | | @elemeta/glasswave/react | React components. | | @elemeta/glasswave/next | React components behind a client boundary. | | @elemeta/glasswave/style.css | Required shared styles. |

Glasswave is a library, not a command-line program. There is no glasswave --help command; use the package imports shown above.

Extended documentation

The complete source documentation is available on GitHub:

License

Licensed under Apache-2.0. See NOTICE for legal notices.