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.
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.
- 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).
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.
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
feGaussianBlurinstead 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
bitmaprenderercanvas 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.
getBoundingClientRectand 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.
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.
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.
- 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>
- 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)
npm install simple-liquid-glassor with yarn:
yarn add simple-liquid-glassimport 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>
);
}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.
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>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.)
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>
);
}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.
| 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 |
<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>// 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.// 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 />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.
- 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
autodetectqualityto let the component choose a preset based on device performance. - Effect Mode: use
effectModeto 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" /><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><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>- 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.
- The component uses
ResizeObserverto adapt to size changes efficiently - SVG filters are hardware-accelerated in modern browsers
- For best performance, avoid animating the glass parameters rapidly
- Use the
presetmode for optimal default settings
autoTextColoruses computed styles from the nearest opaque ancestor to decide betweentextOnDarkandtextOnLight. This helps maintain readable contrast automatically. You can setforceTextColorto enforce the computed color on deeply nested content.
Run a live playground:
npm run storybookSwitch the Backgrounds toolbar between light/dark to see text color adapt in real time.
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.
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.
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.
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.
MIT © [lucaperullo]
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
If you find this package helpful, please consider giving it a star on GitHub!
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.
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.
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:scenePublic 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.
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.
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.
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.
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.
<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.
<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.
<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.
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.
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().
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.
<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.
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.
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.
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.