Skip to content

About

The only zero-dependency liquid glass component with real refraction on iPhone & Safari — not a blur fallback. React 16.8–19 + web-component. ~6.5KB.

Topics

Resources

Stars

30 stars

Watchers

0 watching

Forks

Latest commit

 

History

151 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Simple Liquid Glass

Liquid glass for React 16.8–19, with automatic WebGL refraction on iOS and Android, and SVG optics on desktop.

An Apple-style glass effect with displacement, chromatic aberration, gradient borders, and automatic text color. Includes an optional interactive component and a framework-agnostic web component. Server-rendering and hydration are tested alongside browser behavior.

npm version npm downloads bundle size GitHub stars license

Glass showcase · 📦 npm · 📖 Changelog

Important

For WebGL refraction on phones (or any platform with renderer="webgl"), use LiquidGlassScene or pass backdropRef pointing to a sibling background element. Without either source the component uses frosted CSS.

Why simple-liquid-glass?

  • No additional renderer installation: WebGL and HTML capture are bundled. React is a peer dependency for React entry points.
  • Automatic WebGL refraction on iOS, Android, Safari and Firefox; opt into it on desktop Chromium with renderer="webgl".
  • React 16.8–19 compatibility checked with server rendering, hydration, and unmount tests.
  • A separate web component for other frameworks, with the same iOS WebGL engine.
  • Enforced bundle budgets and browser integration tests in Chromium, Firefox, and WebKit.
  • 60 fps with 104 scrolling panes in Chrome: sibling panes share one WebGL canvas, using less CPU than liquidGL WebGPU (see Performance).

Performance

5.6 renders a page full of glass at display rate. With the library's default look, 104 scrolling panes hold 60 fps in Chrome while using less CPU than liquidGL's WebGPU renderer. In 5.5.1 they ran at 35 fps through SVG and 12 fps through WebGL.

Chrome 156, MacBook Pro M5, Retina (DPR 2), 60 Hz display. Each library uses its own default look. Panes scroll over an image backdrop. Values are frame rate and CPU of the whole Chrome process tree (ms per second), median of four Latin-rotated runs in fresh browsers (two for 5.5.1 WebGL and for 5.6 at 24 panes):

Panes 5.6 renderer="auto" 5.5.1 SVG 5.5.1 WebGL liquidGL WebGL liquidGL WebGPU
24 60 fps · 369 60 fps · 841 36 fps · 648 60 fps · 567 60 fps · 393
48 60 fps · 404 60 fps · 1495 21 fps · 1230 39 fps · 713 60 fps · 462
64 60 fps · 444 60 fps · 1935 17 fps · 1369 30 fps · 578 60 fps · 484
88 60 fps · 531 41 fps · 1803 12 fps · 1358 22 fps · 504 60 fps · 547
104 60 fps · 531 35 fps · 1858 12 fps · 1400 18 fps · 428 60 fps · 587

liquidGL WebGL uses less CPU at 88 and 104 panes only because it draws a third of the frames. Renderer main-thread time stays close to liquidGL WebGPU (22–61 against 37–57 ms/s). It is 4 ms/s higher at 88 and 104 panes.

Each run sees only one renderer on the page, so one slow implementation cannot drag down another. With six panes all of them hold 60 fps. Below 16 panes, renderer="auto" keeps the SVG filter, which samples the live page with no capture. The numbers above vary with backdrop, pane size and GPU. Reproduce them with your own content before relying on them.

How it got faster

Profiling Chrome showed two costs that grow with every pane. First, a backdrop filter's map images are resent to the GPU process every frame the glass or its backdrop moves. Second, every SVG pane is its own GPU render pass. 5.5.1 shrank the first; 5.6 removes the second by drawing every pane into one canvas.

SVG (5.5.1)

  • Maps sized to the pane. Native maps were a fixed 512 px body (604×650 for a 180×120 pane). They now use 0.75 map pixels per CSS pixel (64–512), so far fewer bytes are resent per frame.
  • One map for frost. Frosted analytical glass uses one map and a matched feGaussianBlur instead of sixteen maps.
  • A shorter filter graph. Out-of-backdrop samples are filled with one composite instead of a mask and three composites (pixel-identical).
  • No rebuild storms. Panes moving near the backdrop edge reuse a cached per-shape field and rebuild their map at most every 160 ms.

WebGL (5.5.1 → 5.6)

  • Bitmap transfer. A shared offscreen context renders each pane and hands the frame to a bitmaprenderer canvas instead of copying it into a 2D canvas.
  • One canvas for a crowd. Sibling panes over the same backdrop each render into their own framebuffer in one context. They are then composited into a single canvas placed before the first pane. The browser composites one layer instead of one per pane.
  • No per-frame crops. Fractional pane sizes used to look like a scale change and re-crop and re-upload the backdrop every frame. Exact layout sizes now keep the cached backdrop on the GPU.
  • Sample in place. Panes without blur read the cached backdrop texture directly through a region uniform instead of copying their crop into a private texture.
  • Shared GPU state. Shader programs are compiled once per context and reference-counted. Unchanged uniforms are not resent.
  • One layout read per frame. getBoundingClientRect and scroll offsets are memoised for the frame, so a hundred panes do not each force their own measurement.

Choosing a renderer. On desktop Chrome, renderer="auto" moves 16 or more scene-eligible sibling panes over one backdrop to the shared WebGL scene. It switches back to SVG below 12. Backdrops containing video or iframes stay on SVG, which shows them live. Every other engine already uses WebGL and groups siblings the same way. Set webglScene="off" to keep one canvas per pane.

Automatic page backgrounds with LiquidGlassScene

Available in 5.3.0 through the optional simple-liquid-glass/backdrop entry. Wrap the content you want to refract and the glass surfaces that use it:

import LiquidGlass from 'simple-liquid-glass';
import { LiquidGlassScene } from 'simple-liquid-glass/backdrop';

export function Page() {
  return (
    <LiquidGlassScene>
      <section>{/* HTML, images, video, or canvas */}</section>
      <section>{/* More page content */}</section>
      <LiquidGlass style={{ position: 'fixed', bottom: 20 }}>
        Navigation
      </LiquidGlass>
    </LiquidGlassScene>
  );
}

Phones (iOS, iPadOS, Android) connect automatically. Use renderer="webgl" on a surface to opt in on desktop. Existing backdropRef and backdropSelector props take precedence. SVG-only pages do not start a scene capture.

The scene shares section snapshots, fonts, mutation tracking, and a capture queue across its surfaces. Each visible lens gets a small moving canvas. Use direct-child sections to divide long pages into practical capture regions. A single large wrapper still works but is captured as one region. Keep sticky/parallax content in a separate explicit backdrop; continuously animated HTML styles cannot be reproduced at video-frame speed by cached snapshots.

LiquidGlassScene accepts normal div props and a div ref, plus fontEmbedCSS, maxCacheBytes (default 64 MiB, minimum 8 MiB), and onCaptureError(error, section). Active snapshots may exceed the budget when needed to cover visible surfaces. Fonts/images must be accessible to browser capture; cross-origin media needs suitable CORS headers. Protected media, cross-origin iframes and shadow DOM are not guaranteed to capture.

Auto-connected surfaces are hidden in snapshots while retaining their layout footprint, preventing recursive rendering. Glass with an explicit source can be captured by other scene surfaces. Add data-liquid-glass-ignore to other content you want hidden in the capture. An initial CSS fallback is shown while the first snapshot is prepared.

Nested scenes bind to the nearest provider. Captures are released when the last consumer leaves. Call the glass handle's refreshBackdrop() after external theme/style changes; DOM edits and loads are tracked automatically.

Liquid text and custom renderers

simple-liquid-text 0.3.0 automatically connects to the same provider:

import LiquidGlass from 'simple-liquid-glass';
import { LiquidGlassScene } from 'simple-liquid-glass/backdrop';
import { LiquidGlassText } from 'simple-liquid-text';
import 'simple-liquid-text/styles.css';

<LiquidGlassScene>
  <section>{/* Background content */}</section>
  <LiquidGlassText renderer="webgl">Liquid type</LiquidGlassText>
  <LiquidGlass renderer="webgl">Shared background</LiquidGlass>
</LiquidGlassScene>

For a custom React renderer, useLiquidGlassBackdrop(elementRef, enabled) returns { backdropRef, connected, ready, refresh }. Bind the returned canvas only when connected && ready, and disable this hook when supplying an explicit backdrop. refresh has stable identity.

For non-React integrations, createLiquidGlassBackdrop(root, options) returns a shared capture controller. Call register(element, onReady) to obtain { canvas, refresh, release }, feed canvas to createWebGLSurface, then call release() and destroy() during teardown. The controller does not change renderer selection or mount WebGL outputs.

Choose your path

  • React, simple → import { LiquidGlass } from 'simple-liquid-glass'
  • Pointer-reactive / “alive” → import { LiquidGlassInteractive } from 'simple-liquid-glass/interactive'
  • Vue / Svelte / Astro / plain HTML → import 'simple-liquid-glass/web-component' → <liquid-glass>

Features

  • Liquid glassmorphism with SVG displacement
  • Auto text color: detects dark/light backgrounds to keep text legible
  • Custom glass color: accepts only semi‑transparent colors (rgba, hsla, hex with alpha)
  • Background support: solid colors and gradients with automatic transparency conversion
  • Chromatic dispersion and blur with fine‑grained controls
  • Adjustable saturation to boost or tame color vibrancy
  • Chromatic aberration intensity control to tune the vividness of the edge colors
  • Gradient border with masking
  • Responsive and content‑agnostic
  • TypeScript and tree‑shakable builds (ESM and CJS)

Installation

npm install simple-liquid-glass

or with yarn:

yarn add simple-liquid-glass

Usage

Basic Usage

import React from 'react';
import { LiquidGlass } from 'simple-liquid-glass';

function App() {
  return (
    <div style={{ width: '300px', height: '200px' }}>
      <LiquidGlass autoTextColor background="rgba(255,255,255,0.4)">
        <div style={{ padding: '20px' }}>
          <h2>Your Content Here</h2>
          <p>This content has a liquid glass effect!</p>
        </div>
      </LiquidGlass>
    </div>
  );
}

Phone refraction and desktop opt-in

Pass the background element once. iOS and Android automatically use the optimized WebGL renderer, including native overscroll tracking on iOS. For desktop Chromium, add renderer="webgl". On Android, mobileFallback="css-only" or "svg" keeps the earlier mobile behaviour. iOS uses WebGL even if renderer="svg" or legacy effectMode="svg" is supplied; effectMode="blur" and "off" still explicitly disable refraction.

import { useRef } from 'react';
import { LiquidGlass } from 'simple-liquid-glass';

export function Page() {
  const background = useRef<HTMLDivElement>(null);
  return <>
    <div ref={background}>{/* Your page content */}</div>
    <LiquidGlass
      backdropRef={background}
      renderer="webgl" // Omit to enable WebGL automatically on phones only.
      lensProfile="player"
      lensOptions={{ strength: 0.16 }}
      radius={36}
      style={{ position: 'fixed', bottom: 24, left: 16,
        width: 'calc(100% - 32px)', height: 72 }}
    >
      <nav>Home · Explore · Library</nav>
    </LiquidGlass>
  </>;
}

The source must be a sibling/background, never an ancestor of the glass. backdropSelector is an alternative to a ref. Multiple panels can share the same source; HTML capture and its observers are shared. Normal scrolling only changes the sample coordinates, without recapturing HTML. React panels release their GPU resources when offscreen or unmounted. track is not required for WebGL.

The WebGL renderer uses the package's canonical displacement map and existing lensProfile, lensOptions, scale, displacementScale, radius, dispersion, aberrationIntensity, blur, and saturation controls. As with SVG, low quality disables chromatic separation; otherwise it uses dispersion / 50 * aberrationIntensity for rounded lenses. CSS tint, border, and brightness layers remain configurable. mirror, mirrorScale and track are deprecated and have no effect.

For live video, point backdropRef directly at a <video muted autoPlay playsInline> element. A direct canvas source is live too: it is uploaded once per frame and shared by every panel over it (each samples its own region), so many panels over one animated canvas cost one upload. For HTML sources, text, attributes, images loading, inputs, and size changes refresh the snapshot automatically at a bounded interval, including during continuous updates. Change backdropVersion or call ref.current.refreshBackdrop() after changes outside the subtree, such as a stylesheet/theme change. Refresh retains the previous frame until the new capture is ready. refreshBackdrop() returns a promise and rejects if HTML capture fails.

Capture boundaries: this renders the explicit source, not arbitrary overlapping page layers. Video/canvas nested inside an HTML source are snapshots; pass the media element itself for live frames. Cross-origin media requires CORS permission; protected video and unsupported capture content use the CSS fallback. CSS-only background animations require explicit refreshes; the SVG liquid turbulence animation is not implemented in WebGL. Axis-aligned translation and scale are supported; rotated/perspective source geometry and glass-through-glass compositing are not. HTML captures share identical computed CSS declarations within each detached snapshot, then discard that table. Background/mask resource URLs stay inline for embedding. Trees with internal styles, SVG, custom elements, shadow composition or special media retain the upstream cloning path. The same serializer is used by explicit HTML backdrops and Scene tiles. No stylesheet or DOM-style cache survives between captures.

Shared explicit HTML sources also version their immutable snapshots. When safe section updates are available, an existing GPU texture receives only those regions. Full captures, changed dimensions, missed generations, context restoration or out-of-bounds patches use a full upload. The CPU snapshot remains complete; this does not remove its composition cost. Shader output, capture resolution, scheduling and public options are unchanged.

Long HTML sources use a bounded-resolution snapshot (up to 64 MiB). Keep backdrop regions focused. Visible panels share one offscreen WebGL context; each panel keeps its own composited canvas and local framebuffers. Identical backdrop and lens-map textures share GPU uploads. Avoid unnecessarily large panels because their framebuffers still consume memory.

Without a valid source, WebGL support, or a successful initial capture, the glass remains usable with CSS blur. onDiagnosticsChange/getDiagnostics() report webgl with ios-webgl, mobile-webgl (Android), svg-unsupported (desktop Safari/Firefox) or webgl-requested, or the fallback reason.

Glass mounted on the client after hydration — toasts, popovers, modals — stays transparent until its renderer is ready (at most 400ms), so it never flashes a placeholder look. Server-rendered glass is never hidden and content stays in the accessibility tree. Opt out with reveal="immediate".

Entrance animations: animate transform, not opacity, on elements that wrap the glass. In Chromium an ancestor with opacity < 1 (or filter, mask, clip-path) becomes the backdrop root, so backdrop-filter sees nothing behind it until the fade ends.

Pointer‑reactive elasticity — LiquidGlassInteractive

import { LiquidGlassInteractive } from 'simple-liquid-glass/interactive';

// Leans toward the cursor with a tiny spring. Pass specular to add a tracked highlight.
// Honors prefers-reduced-motion. Core import is unaffected (this is opt‑in).
<LiquidGlassInteractive elasticity={0.3}>…</LiquidGlassInteractive>

Framework‑agnostic web component — <liquid-glass>

For Vue / Svelte / Angular / Astro / plain HTML (no React):

<script type="module" src="https://cdn.jsdelivr.net/npm/simple-liquid-glass/web-component"></script>
<liquid-glass radius="20" frost="0.15" style="width:320px;height:200px;display:block">
  Glass anywhere
</liquid-glass>

Per-framework guides: Vue 3 / Nuxt · Svelte / SvelteKit · Astro · plain HTML / vanilla JS / Angular. (The web component uses backdrop-selector for automatic WebGL on phones; renderer="webgl" enables it on desktop.)

Advanced Usage with Custom Settings

import React from 'react';
import { LiquidGlass } from 'simple-liquid-glass';

function App() {
  return (
    <div style={{ width: '400px', height: '300px' }}>
      <LiquidGlass
        mode="custom"
        scale={200}
        radius={20}
        border={0.1}
        lightness={60}
        displace={0.5}
        alpha={0.8}
        blur={10}
        dispersion={30}
        frost={0.2}
        background="linear-gradient(45deg, #ff6b6b, #4ecdc4)"
        autoTextColor
        textOnDark="#ffffff"
        textOnLight="#111111"
        forceTextColor
        borderColor="rgba(255, 255, 255, 0.5)"
        className="my-glass-container"
        style={{ boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)' }}
      >
        <div style={{ padding: '30px' }}>
          <h1>Custom Glass Effect</h1>
          <p>Fully customized liquid glass morphism with gradient background</p>
        </div>
      </LiquidGlass>
    </div>
  );
}

Preset Mode

The component comes with a beautiful preset that works out of the box:

<LiquidGlass mode="preset">
  <YourContent />
</LiquidGlass>

Note: In preset mode, incoming props still override the preset defaults (e.g., scale, radius, blur, etc.). On iOS, when iosBlurMode is 'auto', a minimal blur (iosMinBlur, default 7px) is applied even if blur is 0 to ensure a visible fallback effect.

The background prop automatically converts solid colors and gradients to semi-transparent (30% opacity) for better glass effects. Images (URLs) are left unchanged.

Props

Prop Type Default Description
children ReactNode - Content to display inside the glass effect
mode 'preset' | 'custom' 'preset' Use preset values or custom configuration
scale number 160 Scale of the displacement effect (-360 to 360)
radius number 50 Border radius of the glass effect
border number 0.05 Border thickness (0 to 0.5)
lightness number 53 Lightness of the glass (0 to 100)
displace number 5 Displacement blur amount (0 to 10)
alpha number 0.9 Alpha transparency (0 to 1)
blur number 0 Blur amount for the glass effect
dispersion number 50 Chromatic dispersion amount
saturation number 140 Color saturation multiplier (%) applied via CSS saturate()
aberrationIntensity number 0 Multiplier for chromatic aberration (red/blue separation)
frost number 0.1 Frost effect intensity (0 to 1)
borderColor string 'rgba(120, 120, 120, 0.7)' Border color in CSS format
glassColor string 'rgba(255, 255, 255, 0.4)' Semi‑transparent glass color (rgba, hsla, hsl(.../a), #RGBA, #RRGGBBAA). Invalid/opaque values fall back to frost‑based default
background string - Background color or gradient (automatically made semi-transparent)
autoTextColor boolean false Automatically detect background luminance and set text color
textOnDark string '#ffffff' Text color used when background is detected as dark
textOnLight string '#111111' Text color used when background is detected as light
forceTextColor boolean false Force the computed text color on all descendants (!important) to override nested styles
className string - Additional CSS class names
style CSSProperties - Additional inline styles
quality 'low' | 'standard' | 'high' | 'extreme' 'low' Rendering quality preset. 'extreme' matches previous versions' visuals
autodetectquality boolean false Auto-detect device performance and pick a quality preset
renderer 'auto' | 'svg' | 'webgl' 'auto' WebGL on iOS, Android, Safari and Firefox; on desktop Chromium SVG, or the shared WebGL scene for 16+ sibling panels; force either with svg/webgl
webglScene 'auto' | 'off' 'auto' Sibling WebGL panels over one backdrop share one canvas. 'off' keeps one canvas per panel (and SVG on Chromium)
backdropVersion string | number — Change to refresh cached HTML
maxScale number — (device pixel ratio, at most 3; 2 for liquidGL optics) Upper bound for the WebGL drawing scale in device pixels per CSS pixel, e.g. 1.5 on phones to shade about a third of the pixels of a 2.6× screen. A shared scene composites at the largest maxScale of its panels. SVG and CSS rendering are unaffected
mobileFallback 'css-only' | 'svg' WebGL on mobile Override the mobile renderer: CSS blur or the SVG filter
effectMode 'auto' | 'svg' | 'blur' | 'off' 'auto' Control effect: auto, force SVG, force CSS blur, or disable

Examples

Background with Gradient

<LiquidGlass 
  background="linear-gradient(45deg, #ff6b6b, #4ecdc4)"
  autoTextColor
>
  <div style={{ padding: '20px' }}>
    <h2>Gradient Background</h2>
    <p>This uses a gradient background that's automatically made semi-transparent!</p>
  </div>
</LiquidGlass>

Quality Presets and Autodetection

// Manual quality
<LiquidGlass quality="high" background="rgba(255,255,255,0.35)">
  <Content />
</LiquidGlass>

// Autodetect device performance and choose quality automatically
<LiquidGlass autodetectquality background="rgba(255,255,255,0.35)">
  <Content />
</LiquidGlass>

// Note:
// - Default quality is 'low'.
// - 'extreme' produces the same visual fidelity as previous versions of this component.
// - For many instances on the same page, prefer quality='low' or autodetectquality.

Mobile Fallback Control

// Force CSS-only (strongest performance) even on desktop
<LiquidGlass mobileFallback="css-only" />

// Force SVG filter on mobile (may impact performance)
<LiquidGlass mobileFallback="svg" />

// Default behavior: CSS-only on mobile, SVG on desktop
<LiquidGlass />

Browser Support & Rendering Strategies

SVG filters inside backdrop-filter (url(#...)) only work in Chromium (Chrome, Edge, Opera, Android Chrome). Safari/iOS WebKit (bug 245510) and Firefox silently ignore them, so the component picks a strategy per engine:

Engine effectMode="auto"
Chromium desktop SVG displacement (full effect); 16 or more sibling panels over one backdrop render through the shared WebGL scene
Chromium Android WebGL with LiquidGlassScene or backdropRef; frosted CSS otherwise (mobileFallback selects CSS or SVG)
iOS / iPadOS browsers WebGL with LiquidGlassScene or backdropRef; frosted CSS otherwise
Desktop Safari WebGL with LiquidGlassScene or backdropRef; frosted CSS otherwise
Firefox WebGL with LiquidGlassScene or backdropRef; frosted CSS otherwise

Frosted CSS fallback: blur, tint, saturation, and inset highlights provide the glass appearance when no usable WebGL backdrop is provided. Performance depends on lens size, scene complexity, and device.

The current WebGL renderer integrates the optimized direct-output pipeline tested in the scenario lab. It is distinct from the older removed experiment.

Performance and Fallbacks

  • Default behavior: on mobile devices the component uses a CSS-only effect to avoid jank; on desktop Chromium it uses the SVG filter, and moves 16 or more sibling panels over one backdrop to the shared WebGL scene (see Performance).
  • Quality presets: 'low' is the default and optimized for many instances; 'extreme' matches previous visuals.
  • Autodetection: set autodetectquality to let the component choose a preset based on device performance.
  • Effect Mode: use effectMode to force the strategy.
// Force CSS-only blur (no SVG) — recommended on very low-end devices
<LiquidGlass effectMode="blur" />

// Disable all filter effects (keeps border/frost/background)
<LiquidGlass effectMode="off" />

// Request SVG refraction on supported engines
<LiquidGlass effectMode="svg" />

Card with Glass Effect

<div className="card-container" style={{ 
  width: '350px', 
  height: '200px'
}}>
  <LiquidGlass 
    radius={15} 
    frost={0.15} 
    autoTextColor 
    background="linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
  >
    <div style={{ padding: '24px', height: '100%' }}>
      <h3 style={{ marginBottom: '12px' }}>
        Glass Card
      </h3>
      <p>
        This is a beautiful glass morphism card with liquid distortion effects and gradient background.
      </p>
    </div>
  </LiquidGlass>
</div>

Overlay Effect

<div style={{ position: 'relative', width: '100%', height: '400px' }}>
  <img src="background.jpg" alt="Background" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
  
  <div style={{ 
    position: 'absolute', 
    top: '50%', 
    left: '50%', 
    transform: 'translate(-50%, -50%)',
    width: '300px',
    height: '150px'
  }}>
    <LiquidGlass autoTextColor background="rgba(255,255,255,0.4)">
      <div style={{ padding: '20px', textAlign: 'center' }}>
        <h2>Overlay Content</h2>
        <p>Glass effect over image</p>
      </div>
    </LiquidGlass>
  </div>
</div>

Browser Support

  • Chrome (latest)
  • Firefox (latest)
  • Safari (latest)
  • Edge (latest)

The component leverages modern CSS (backdrop-filter) and SVG filters. Older browsers may not support all effects.

Performance Tips

  1. The component uses ResizeObserver to adapt to size changes efficiently
  2. SVG filters are hardware-accelerated in modern browsers
  3. For best performance, avoid animating the glass parameters rapidly
  4. Use the preset mode for optimal default settings

Accessibility

  • autoTextColor uses computed styles from the nearest opaque ancestor to decide between textOnDark and textOnLight. This helps maintain readable contrast automatically. You can set forceTextColor to enforce the computed color on deeply nested content.

Storybook

Run a live playground:

npm run storybook

Switch the Backgrounds toolbar between light/dark to see text color adapt in real time.

Engine API (experimental)

simple-liquid-glass/engine is a public package entry point starting in 5.7.0, with ESM, CommonJS and TypeScript exports. Install it with npm install simple-liquid-glass@~5.7.0 to stay on this experimental minor line; review the changelog before upgrading to a new minor. No local build alias is needed.

It is a framework-agnostic extension API for custom WebGL materials — your own private GLSL, running on this package's shared WebGL context, frame loop and backdrop leases. It is @experimental: its types and behaviour may change in a minor release while ENGINE_API stays 1 (see the CHANGELOG's "Engine API" section for every change). It has no React import and never imports, detects or special-cases any other package.

import { getGlassEngine, type DrawResult } from 'simple-liquid-glass/engine';

const engine = getGlassEngine(); // null on the server, or without WebGL
if (engine) {
  const material = engine.defineMaterial<{ color: [number, number, number, number] }>({
    id: 'my-app/tint@1',
    fragment: (caps) => `
      uniform vec4 u_color;
      vec4 slg_shade(vec2 p) {
        vec3 backdrop = slg_backdrop(slg_toScreen(p));
        return vec4(mix(backdrop, u_color.rgb, u_color.a), 1.0);
      }
    `,
    inputs: { backdrop: true },
    draw(ctx, params): DrawResult {
      ctx.uniform('u_color', ...params.color);
      ctx.draw();
      return 'drawn';
    },
  });

  const surface = engine.createSurface({
    host: document.querySelector('#panel')!,
    material,
    params: { color: [1, 1, 1, 0.25] },
    backdrop: document.querySelector('#background')!, // <video>/<canvas> sampled in place, HTML captured, or { scene: true }
  });

  surface.update({ color: [0.2, 0.6, 1, 0.3] }); // redraws once, next frame
}

A fragment defines vec4 slg_shade(vec2 p); the engine supplies the rest of the program (vertex shader, main() and the slg_* prelude: slg_view, slg_scale, slg_origin, slg_viewport, slg_time, slg_point(), slg_toScreen(p), slg_hasBackdrop(), slg_backdrop(screen), slg_hasUnder(), slg_under(screen)). draw() returns one of 'drawn', 'unchanged', 'pending' (present this frame, ask again next frame — a loading backdrop) or 'resized' (redraw now at a freshly re-read bleed()). defineResource() gives one instance per WebGL context (a shared LUT or atlas, re-created lazily after a context loss). createSurface()'s active() option keeps a surface drawing every frame (a spring, say); otherwise it only redraws when its params, geometry or backdrop actually change. A material that throws on 3 consecutive frames falls back to status: { state: 'fallback', reason: 'material-error' } and stops drawing, without affecting any other surface.

Layers, glass on glass and custom backdrops

const root = document.querySelector('#page')!;
const layer = engine.createLayer(root, { under: true }); // under/snapshots are opt-in, off by default

// Any descendant surface joins the nearest enclosing layer automatically (composition: 'auto', the default);
// pass `layer` explicitly, or `composition: 'own'` to opt a surface out even inside a layer's root.
const back = engine.createSurface({ host: document.querySelector('#card')!, material, params });
const front = engine.createSurface({ host: document.querySelector('#badge')!, material: underAwareMaterial, params, layer });
// front's slg_under(screen) now reads back's glass wherever they overlap, in document order.

engine.registerBackdropSource({
  id: 'my-app/page-model@1',
  paint(ctx, region, scale, exclude) { /* paint `region` (viewport CSS px) into ctx at `scale`, skipping `exclude` */ return { drew: true, live: false }; },
  version: () => myPageModelVersion,
});
const custom = engine.createSurface({ host: document.querySelector('#hud')!, material, params, backdrop: { source: 'my-app/page-model@1' } });

createLayer(root, { scale?, under?, snapshots?, revealMs? }) draws its members in document order into one shared canvas placed as root's first child, z-index: -1 — ordinary sibling content still paints over it, so glass sits correctly behind whatever the DOM says is in front of it. There is no eligibility filter: any descendant qualifies, including a z-index: -1 host inside an isolated root. under: true lets a later member refract the glass already drawn beneath it (one extra buffer copy per member); snapshots: true also makes that available, as a downscaled last-frame copy, to a floating (composition: 'own') surface near the layer. Both are off by default; stock <LiquidGlass> never turns them on. registerBackdropSource lets a private page model (Pro's pageScene, for example) supply a backdrop without ever living in this MIT engine: backdrop: { source } samples it exactly like a <canvas> or { scene: true } backdrop, repainted only when version() changes or the surface's view would read outside the last painted strip.

Array, integer and sampler uniforms

fragment: () => `
  uniform int uMode;      // ctx.uniform() detects this is an int and sends it with uniformNi, not uniformNf
  uniform vec4 uRect[8];  // one ctx.uniformArray() call for the whole array, instead of 8 named uniforms
  vec4 slg_shade(vec2 p) { /* ... */ }
`,
draw(ctx, params) {
  ctx.uniform('uMode', params.mode); // works for float/vecN and int/bool/sampler2D/samplerCube alike
  ctx.uniformArray('uRect', params.rects, 4); // components: the GLSL type's arity, 1-4
  ctx.draw();
  return 'drawn';
},

ctx.uniform() reads each uniform's GLSL type once per program (getActiveUniform, right after linking) and picks uniformNf or uniformNi accordingly — an int/bool/sampler2D/samplerCube uniform set through the float entry point raises GL_INVALID_OPERATION on a real driver, so this is not optional plumbing. ctx.uniformArray(name, values, components) is the array counterpart: one call (with the same per-program location cache as uniform()) instead of one uniform() per slot, for things like a packed uRect[MAX_SLOTS]. It is not value-diffed the way uniform() is — array data is expected to change every draw.

ctx.draw(rects) can draw past the surface's own view

A rect given to ctx.draw(rects) may legally extend past ctx.view's own edges — a material's soft rim or refraction reach that needs a few extra px beyond its own box, for example. gl.viewport widens once for the whole call, to the union of the view and every rect, when one of them reaches past it; each rect is then scissored inside that union exactly as a rect fully inside the view already was. slg_origin/slg_view/ slg_scale are never touched by ctx.draw itself, so p (slg_point()) stays identical to the unpadded case for every pixel, in the view and in the bleed alike — a shape a material positions in surface coordinates (an SDF slot, say) is never shifted or stretched by asking for a rect past the edge. It is still clipped to the real framebuffer: how far past the view a rect can actually reach depends on room already there. A layer member's shared buffer typically has plenty; an own-canvas surface's canvas is sized to exactly its own (bleed-inclusive) view, so ctx.draw(rects) can never reach further than that — bleed is how to get more room there, not a rects entry past it.

What this release does not include. G9 (a frost input) and G11 (pointer input) are not implemented: there is no slg_frost/slg_pointer and no pointer field anywhere in this API. See the CHANGELOG's "Engine API" entry for the full list, every slg_* prelude function, and an MIT example material at tests/engine/tint-material.ts.

License

MIT © [lucaperullo]

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/AmazingFeature)
  3. Commit your changes (git commit -m 'Add some AmazingFeature')
  4. Push to the branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

Support

If you find this package helpful, please consider giving it a star on GitHub!

Keywords

react, react component, react ui, glassmorphism, glass morphism, liquid glass, liquid-glass, glass effect, frosted glass, frosted-glass, blur, blur effect, backdrop-filter, svg filter, displacement, displacement map, chromatic aberration, ui effects, card, overlay, glass ui, glass card, glass panel, glassmorphism react

For issues and feature requests, please create an issue.

Compatibility and validation

React 16.8/17 uses a mount-time ID fallback; React 18/19 uses React's hydration-safe IDs. Browser-dependent effects activate after hydration. When using multiple React 18+ roots, provide distinct React identifierPrefix values on the server and client.

The JavaScript build targets Chrome 64, Firefox 69, and Safari 12 syntax. Actual visual effects depend on browser CSS/SVG support; automated browser checks run against current Chromium, Firefox, and WebKit, not every historical release. Missing ResizeObserver falls back to window resize events.

effectMode="off" removes backdrop filtering. effectMode="blur" disables WebGL and uses the CSS fallback. The web component can be imported during SSR; instantiate it only in a browser. Install React and React DOM explicitly when using a React entry point; the web component needs neither.

Development checks

npm ci
npm run typecheck
npm test -- --runInBand
npm run build
npm run size
npm run test:compat
npx playwright install chromium firefox webkit
npm run test:browser
npm run test:webgl
npm install --prefix packages/simple-liquid-glass-font
npm run build --prefix packages/simple-liquid-glass-font
npm run test:scene

Public TypeScript declarations are generated from source during the build. Bundle budgets use Brotli, not gzip. CI also checks React 16.8.6, 17.0.2, 18.3.1, and 19 in isolated installations.

The 5.5.0 Brotli budgets are 47.3 KB for core, 49.3 KB for interactive, 47.3 KB for the deprecated mirror alias, and 35.7 KB for the web component (5.4.0: 30 / 33 / 31 / 23 KB). Most of the increase is the forked HTML capture pipeline and the analytical optics. The optional backdrop and experimental (31 KB) entries are separate. Run npm run size to measure the current builds.

Rounded native refraction (Chromium)

refraction="lens" uses the MIT-licensed rounded lens generator and native SVG material filter adapted from samasante/liquid-glass. The pinned source revision, adaptations and license are recorded in THIRD_PARTY_NOTICES.md and included in the published package. No UI branding or runtime dependency is added.

<LiquidGlass refraction="lens" quality="high" radius={32} scale={160}
  blur={2} saturation={115} aberrationIntensity={0.32}
  glassColor="rgba(255,255,255,0.06)">
  Navigation
</LiquidGlass>

Set lensProfile="loupe" for the stronger playground optics (14% baseline strength, deeper curvature and brighter sheen). The default lensProfile="player" uses the stronger player shape at 80 CSS pixels of displacement. lensProfile="material" remains available for gentler surface settings.

The existing prop names remain. For this mode, scale={160} maps to the material's baseline strength (80 CSS pixels for the default player profile); 0 disables displacement, and 320 doubles it. dispersion={50} with aberrationIntensity={0.32} produces 0.32 normalized color separation. Low quality uses one displacement pass. Blur precedes refraction, and the map's blue channel supplies the directional sheen. The 512×512 map is cached by geometry; strength, blur and color adjustments reuse it.

The rounded lens is now the default. The original gradient look is available with refraction="classic", with strength bounded to 10% of the shorter side to prevent extreme tearing. Native filters have explicit sampling bounds and neutral padding. iOS uses the integrated WebGL renderer described above; the library demo covers both static HTML and live video.

For matching a video surface's refraction strength, set displacementScale to strength * Math.hypot(surfaceWidth, surfaceHeight) / Math.SQRT2. This optional CSS-pixel override applies only to refraction="lens", bypassing the preset scale calculation. Omit it to retain existing behavior.

Upgrading from 4.1

Version 5 changes the React default to a rounded player lens with 80px base displacement. scale={160} is the baseline; 0 disables it and 320 doubles it. Existing angle, shapeAdapt, lens, lensStrength, and lensCenter props select the compatible 4.x renderer automatically. liquid, liquidSpeed, and liquidScale remain supported, including reduced-motion handling. The web component retains its 4.1 directional and animated controls and appearance. Experimental GPU/video components are not included.

Individual lens controls (5.1)

Use lensOptions to override any part of a rounded lens. Unset values inherit lensProfile (default: player); existing applications retain their appearance.

import { LiquidGlass, type LensOptions } from 'simple-liquid-glass';

const optics: LensOptions = {
  strength: 0.14, depth: 0.15, curvature: 0.45,
  bend: 0.55, bendWidth: 0.3,
  sheen: 2, sheenWidth: 10, specular: 1.6, sheenAngle: 0,
  glow: 0.1, brightness: 0,
};

<LiquidGlass refraction="lens" lensProfile="player" lensOptions={optics}
  quality="extreme" blur={1} radius={32}>
  <YourContent />
</LiquidGlass>
Option Range Meaning
strength 0–0.5 Relative displacement strength
depth 0–1 How far refraction reaches inward
curvature 0–1 Body dome curvature
bend 0–1 Inner-edge bend intensity
bendWidth 0.001–0.5 Edge-band width relative to the smaller dimension
sheen 0–2 Directional sheen intensity
sheenWidth 0–10 Sheen thickness in pixels
sheenFalloff 0.1–5 Sheen falloff exponent
sheenAngle -360–360 Light direction in degrees
specular 0–3 Overall highlight gain
glow 0–1 Inner glow intensity
glowSpread 0.01–2 Inner glow reach
glowFalloff 0.1–5 Inner glow falloff exponent
brightness -1–1 Black-to-white surface veil opacity

displacementScale, when provided, takes precedence over lensOptions.strength. The existing scale multiplier still applies to relative strength. blur, frost, dispersion, and aberrationIntensity remain top-level props. lensOptions affects refraction="lens" on React components, including the interactive export. It does not alter the classic renderer or add optical refraction to the blur fallback. The brightness veil also works on that fallback. The web component does not expose this API.

LENS_PROFILES, LENS_OPTION_RANGES, and resolveLensOptions(profile, overrides) are exported for building settings panels without duplicating defaults. Values are clamped to these ranges; non-finite values inherit the selected profile. Geometry changes invalidate cached maps; strength, specular, and brightness do not.

Interactive controls

The /interactive entry also accepts liquidTrigger="always" | "hover" | "press", followPointer, clickRipple={true | "ripple" | "drop"}, and rippleIntensity. specular is opt-in and defaults to false. Click ripples are clipped surface overlays; they do not capture or refract the backdrop themselves.

Material presets

<LiquidGlass material="smoked">Your content</LiquidGlass>
<LiquidGlass material="frosted" blur={4} glassColor="rgba(220,230,255,0.3)">
  A customized finish
</LiquidGlass>

material accepts clear, frosted, smoked, or subtle. Each coordinates the lens profile, blur, saturation, color separation, frost, tint, and border color. Explicit props override individual preset settings; undefined inherits the preset. Omitting material preserves the existing defaults. Rendering quality and browser fallback rules still apply, including the low-quality blur cap. Material selection does not choose text colors: set those for your content and background.

MATERIAL_PRESETS and the MaterialPreset type are exported for material pickers and design tools. React wrappers inherit the same material prop. These additions are currently part of the React API, not the web component.

Rendering diagnostics

<LiquidGlass
  onDiagnosticsChange={({ strategy, reason, quality }) => {
    console.log({ strategy, reason, quality });
  }}
/>

The callback runs after mounting and when the selected renderer, reason, or quality changes. ref.current.getDiagnostics() reads the current snapshot; getQuality() continues to work. The root also exposes data-glass-strategy and data-glass-reason for inspection. These report the library's rendering decision, not a guarantee of browser support, visual fidelity, or frame rate.

Strategy Meaning
pending Waiting for client initialization
off Effects explicitly disabled
paused Offscreen effects paused
svg Native SVG backdrop refraction
webgl WebGL backdrop refraction
blur CSS fallback

Reasons include effect-disabled, offscreen, blur-requested, native-svg, ios-webgl, svg-unsupported, webgl-requested, webgl-unavailable, capture-failed, missing-backdrop, invalid-selector, invalid-backdrop, and initializing. For missing-backdrop, supply a sibling background using backdropRef or backdropSelector. For invalid-backdrop, move the source outside the lens and its ancestors. RenderingDiagnostics, RenderingStrategy, and RenderingReason are exported TypeScript types.

WebGL in Vue, Svelte, Astro, Angular or plain HTML

<script type="module">
  import 'simple-liquid-glass/web-component';
</script>
<div id="background">Your page content</div>
<liquid-glass backdrop-selector="#background" renderer="webgl"
  lens-profile="player" strength="0.16" radius="36"
  style="position:fixed;bottom:24px;left:16px;width:320px;height:72px">
  Home · Explore · Library
</liquid-glass>

Omit renderer for automatic iOS selection. WebGL attributes include lens-profile, strength, dispersion, radius, scale, blur, saturation, backdrop-selector, backdrop-version, and max-scale (the maxScale prop). effect-mode="blur" or "off" explicitly disables refraction. Call element.refreshBackdrop() for a manual capture refresh. Disconnecting the element releases its framebuffers and source subscription; the shared context is released when the last surface disconnects. Geometry and optical attribute updates reuse the existing engine. lens-profile and strength now select the same native lens on Chromium and WebGL; explicit legacy directional/lens attributes keep the classic gradient path.

The bundled renderer increases the React core to approximately 50 KB Brotli, including HTML capture and the shared scene (React excluded); the web component is approximately 36 KB Brotli. No separate runtime dependency installation is required. See THIRD_PARTY_NOTICES.md for bundled licenses. Run npm run test:webgl for the library integration/overscroll tests in Chromium and WebKit.

Shared WebGL surfaces

simple-liquid-glass/webgl exports the framework-independent createWebGLSurface(element, output, backdrop, options, onStatus) engine and its TypeScript types. It accepts a displacement map URI, scale, dispersion, radius, blur and saturation; the output holder can be CSS-masked. additiveDispersion and neutralPoint support glyph maps; maxScale caps the drawing scale. The returned surface exposes update(options), refresh() and destroy(). This is the canonical engine used by simple-liquid-text, including overscroll tracking and background capture lifecycle.

Page-level stats for benchmarks

getGlassEngineStats() from simple-liquid-glass/webgl reports how every glass surface on the page renders and what the WebGL engine did in its last frame. It covers LiquidGlass, <liquid-glass> and createWebGLSurface surfaces from any entry on the page.

import { getGlassEngineStats } from 'simple-liquid-glass/webgl';
getGlassEngineStats(); // starts counting
requestAnimationFrame(() => {
  const s = getGlassEngineStats();
  // s.strategies: { 'webgl-scene': 44, 'webgl-single': 0, svg: 0, 'css-fallback': 4, pending: 0, paused: 0, off: 0 }
  console.log(s.refracting, s.contexts, s.drawn, s.missed, s.uploads, s.uploadBytes, s.frameMs, s.total);
});
Field Meaning
contexts WebGL contexts used by live surfaces (the shared offscreen context, one per shared scene)
surfaces, webgl, refracting Glass surfaces on the page; live WebGL surface engines; surfaces refracting through WebGL or SVG
strategies, list Count and per-element { element, strategy, reason }, with strategy webgl-scene, webgl-single, svg, css-fallback, pending, paused or off, and the diagnostics reason or WebGL status
visible, drawn, missed In the last engine frame: WebGL surfaces in the viewport, surfaces that rendered, visible surfaces that needed a frame and could not draw one (snapshot or video frame not ready, a fault). Unchanged surfaces skip their frame and are not missed
uploads, uploadBytes Texture uploads in the last frame (backdrops, crops, masks, lens maps)
frameMs Main-thread time of the engine's frame callback (measurement, drawing, scene compositing), at performance.now() resolution
frames, total Engine frames since counting started, and the cumulative frame fields

Counting starts with the first call; until then the engine counts nothing, and afterwards a frame costs a few counter increments. If window.__benchExpose is set before the entry loads, it is also available as window.__slgEngine.stats().

Capture and rendering optimizations (5.5)

HTML snapshots patch only changed opaque, clipped, non-overlapping sections. Relational selectors, masks, rounded roots, filters, overflowing/fixed content and uncertain styles fall back to a complete capture. Manual refresh consumes pending mutation notifications to avoid duplicate captures. Scene captures overlapping sections as one root to preserve section stacking.

The native SVG fast path precomposes a neutral border once into the map PNG when specular and dispersion are disabled. Since 5.5.1 SVG maps are sized to the pane (0.75 map pixels per CSS pixel, 64–512); WebGL keeps 512. Highlighted, chromatic, liquid and very small controls retain the general filter graph. A zero WebGL blur no longer downscales the source; nonzero blur retains its existing kernel.

The public entry points and controls are unchanged. A panel with its own canvas presents through canvas[data-liquid-glass-webgl] (a bitmaprenderer canvas when bitmap transfer is supported, otherwise 2D); panels in a shared scene draw into canvas[data-liquid-glass-scene] placed in their parent and carry data-glass-composition="scene".

Brotli budgets rise in 5.5 to include the analytical optics, shared capture and rendering implementation (see Compatibility and validation). Run npm run size for measured values.

Analytical WebGL optics (5.5)

<LiquidGlass renderer="webgl" webglOptics="liquidgl" /> selects an analytical shader adapted from liquidGL (MIT, see THIRD_PARTY_NOTICES.md). The existing map-based optics remain the default. Source capture, observer/texture sharing, context pooling, presentation canvases, error fallback and cleanup remain managed by this package.

<LiquidGlass
  className="liquidgl-match"
  renderer="webgl" webglOptics="liquidgl" backdropRef={backdropRef}
  radius={24} blur={0} saturation={100} frost={0} glassColor=""
  borderColor="rgba(0,0,0,0)"
  liquidGLOptions={{ refraction: .006, bevelDepth: .015, bevelWidth: .15,
    frost: 0, aberration: 0, specular: false }}
><div className="content">Content</div></LiquidGlass>

The coefficients use liquidGL's units, independently of lensOptions / displacementScale. liquidGLOptions.frost defaults to blur / 8 for this mode; it uses the source-sampling frost kernel, not the native separable blur. The model includes rounded-box refraction, corner normals, a five-tap sharp sampler, 16-tap frost, chromatic aberration and moving specular highlights. It samples the complete backdrop rather than a local crop; sources larger than the GPU texture limit use the existing CSS fallback. DPR is capped at 2, as in the reference. The WebGL-only selector leaves SVG unchanged; use the generic optics prop described below to opt SVG into the analytical model. The custom element retains native optics.

This does not import liquidGL's DOM capture, stacking, fluid interaction, tilt, magnification, shadow or content-layer engine. Those capabilities are not enabled by this option. Pixel equality depends on capture and compositing too: translucent input retains this package's straight-alpha correctness; independently composited moving canvases and different DOM/video capture can differ from liquidGL. Fidelity is measured per workload; it is not a guarantee of bit-identical output.

To reproduce a liquidGL comparison panel rather than this component’s native decorative shadows, use:

.liquidgl-match[data-liquid-glass] > div { box-shadow: none !important; }
.liquidgl-match .content { padding: 12px; font: 14px Arial; color: #15263a; }

Set the same panel size/position and backdrop as the reference. The demonstrated coefficients (.006 / .015 / .15) match the comparison panel; they do not automatically inherit liquidGL’s upstream defaults. Run the analytical WebGL acceptance suite with LIQUID_GLASS_TEST_OPTICS=liquidgl node experiments/library-webgl/test.mjs.

CPU presentation optimization (5.5)

The analytical mode can render through one shared OffscreenCanvas WebGL context and transfer each completed frame as an ImageBitmap to its existing 2D presentation canvas. A small, cached capability probe checks mixed-color alpha before this path is enabled; unsupported or incorrect implementations use the original canvas-copy path. ImageBitmaps are closed after drawing. Native/legacy and analytical transfer paths can use two shared contexts when combined; neither allocates a WebGL context per panel. The public component and 2D output canvas APIs remain available.

Geometry reads are grouped before presentation writes, vertex attribute locations are cached, and unchanged program uniforms/texture leases are reused. All cached GL state is reset on context restoration. The actual GLSL fragment string, refraction model, 16 frost samples, resolution and DPR are unchanged. Improvements vary by workload; some scenes are not faster than 5.4.

Analytical optics in the native SVG path (5.5)

Use <LiquidGlass renderer="svg" optics="liquidgl" backdropRef={backdropRef} liquidGLOptions={{refraction:.006,bevelDepth:.015,bevelWidth:.15,frost:0}} />. The generic optics prop also selects the model for WebGL, and overrides the existing WebGL-only webglOptics prop. Existing defaults and the legacy selector are unchanged. This extension is available in the React component and its wrappers; the custom element retains its existing optics.

Chromium implements this with SVG backdrop filters: cached analytical displacement maps, a five-tap cross kernel at frost zero, sixteen displaced samples for frost, optional RGB separation and moving highlights. The SVG path does not create a WebGL context or capture HTML to a bitmap. The SVG backdrop samples the composed DOM behind the panel; backdropRef supplies its coordinate system, not an isolated bitmap or a content boundary. Overlapping foreground elements can therefore differ from an explicit WebGL capture. Equal interior panes share maps; source-edge movement and changes to source size, pane size, scale, DPR or optics invalidate them. Generation and cache retention are bounded (64 MiB decoded map estimate per asset/cache, 32 cached groups, 4096-pixel maximum map dimension); mounted filter images and browser intermediate surfaces are additional memory. Over-budget maps use the CSS blur fallback. Invisible/off panes unsubscribe from the shared frame loop.

The analytical field is translated from the WebGL model, but this is an approximation of its rendered pixels: SVG maps are 8-bit, frost random functions and premultiplied-alpha filtering differ, and browsers choose their own intermediate surface precision. Do not claim pixel identity. Frost and aberration add many SVG primitives and can be slower than WebGL. Specular highlights are raster approximations of the same moving discs. Firefox and WebKit cannot use SVG filters in backdrop-filter: the existing WebGL fallback is selected, now with the same analytical model. getDiagnostics() reports strategy: 'svg', reason: 'liquidgl-svg' only for the actual SVG path.

Experimental shared scene — simple-liquid-glass/experimental

createExperimentalScene(overlay, panels, source, options?) renders analytical liquidGL refraction for a fixed list of flat panels through one shared WebGL canvas, instead of one presentation canvas per panel. It is not used by the ordinary entry points, and its API may change in a minor release.

import { createExperimentalScene } from 'simple-liquid-glass/experimental';

const scene = createExperimentalScene(overlay, [...overlay.querySelectorAll('.panel')], background, {
  radius: 24, blur: 0,
  liquidGL: { refraction: .006, bevelDepth: .015, bevelWidth: .15 },
});
scene.status(); // [{ strategy: 'active', backend: 'shared-webgl', submission: 'instanced' }, …]
await scene.refresh(); // after source changes a MutationObserver cannot see
scene.destroy();

The overlay must be positioned and must not contain, or be contained by, the source. The glass is composited below all overlay content, so nested clips, interleaved z-order, rotated panels and glass-through-glass are not supported. Recreate the scene when the panel list changes. Panels are drawn into one instanced atlas (limited to 2 Mpixel); video, fractional DPR and large surfaces use a per-panel path in the same context.

For long, changing HTML sources the capture scale can be raised by at most 1% above the 16-Mpixel budget so that section updates land on whole device pixels and can be uploaded as GPU patches. This changes resampling and antialiasing of the captured raster compared with 5.4 (the optical mapping is unchanged); it does not produce pixel-identical output. If the larger allocation fails on the CPU or GPU, the scene skips frames, recaptures at the original scale and keeps that scale for the source. Calibrated native canvas text is used only after exact equality checks against the SVG rasterizer; otherwise the SVG path remains. Calibration is a one-time cost, measured at roughly 0.35 s of main-thread work for a 1200×4000 Retina source in Chrome.

About

The only zero-dependency liquid glass component with real refraction on iPhone & Safari — not a blur fallback. React 16.8–19 + web-component. ~6.5KB.

Topics

Resources

Stars

30 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages