Back to Articles

Liquid Glass JS: Multi-Layer WebGL Refraction for Apple-Inspired Glass Effects

[ View on GitHub ]

Liquid Glass JS: Multi-Layer WebGL Refraction for Apple-Inspired Glass Effects

Hook

While CSS backdrop-filter gives you a blur, Liquid Glass JS gives you refraction—the actual bending of light through glass that makes Apple's design language feel tangible rather than flat.

Context

The glass morphism trend exploded after Apple's WWDC 2020 showcase of Big Sur's translucent UI elements, spawning thousands of Dribbble mockups featuring frosted glass cards with blurred backgrounds. CSS backdrop-filter became the go-to implementation, but it only handles the blur component of glass effects. Real glass doesn't just blur what's behind it—it refracts light, bending and distorting the background in ways that respond to the glass shape's curvature. Edge highlights catch light differently than centers. Nested glass layers interact, with each pane refracting through the ones beneath it.

Liquid Glass JS emerged to bridge this gap between flat backdrop-filter implementations and the sophisticated glass rendering that requires understanding surface normals, multi-layer shader composition, and real-time background sampling. Rather than settling for the blur-only aesthetic that flooded the web after Big Sur, it targets developers building premium experiences where the physicality of glass matters—landing pages for design tools, portfolio hero sections, or luxury brand sites where visual fidelity separates good from exceptional.

Technical Insight

Shader Layers

element reference

initialize

captures page

background texture

manages

renders

renders

renders

calculates normals

distance fields

composites

composites

composites

updates

parent output

samples parent texture

extends

interactive events

DOM Element

Container Class

WebGL 2.0 Context

html2canvas

Rendering Pipeline

Edge Layer

High Refraction

Rim Layer

Specular Highlights

Base Layer

Gaussian Blur 13x13

Shape Module

roundedRect/circle/pill

Final Frame

Uniform Parameters

intensity/blur/distortion

Child Container

Nested Glass

Button Class

System architecture — auto-generated

The library's architecture centers on WebGL 2.0 fragment shaders that calculate refraction per-pixel based on shape-aware surface normals. Unlike simple blur effects, Liquid Glass JS computes how light would bend through glass by determining the perpendicular vector at each pixel position. For rounded rectangles, this means calculating distance fields that account for corner radii, then deriving gradients that change smoothly from flat surfaces to curved edges.

The rendering pipeline consists of three distinct shader layers that composite together. The edge layer samples the background with high-intensity refraction distortion, creating the characteristic light-bending effect at glass boundaries. The rim layer adds specular highlights that simulate light catching the glass surface at grazing angles. The base layer provides the primary refraction with Gaussian blur applied through a 13×13 kernel sampler. Each layer receives uniform parameters for intensity, blur radius, and distortion strength, enabling real-time tuning without shader recompilation.

Here's how you initialize a glass container with custom refraction parameters:

import { Container } from 'liquid-glass-js';

const glassCard = new Container({
  element: document.querySelector('#hero-card'),
  shape: 'roundedRect', // 'circle' or 'pill' also supported
  cornerRadius: 24,
  edgeIntensity: 0.8,
  rimIntensity: 0.6,
  blurRadius: 12,
  refractionStrength: 0.15,
  tint: [1.0, 1.0, 1.0, 0.1] // RGBA, slight white tint
});

glassCard.render();

The shape-aware normal calculation is where the physics comes alive. For a rounded rectangle, the library computes distance fields that treat corners as circular arcs and edges as linear segments. At each pixel, it determines whether you're sampling from a corner region (where normals point radially from the corner center) or an edge region (where normals are perpendicular to the edge). This distinction ensures refraction distortion follows the actual curvature of your glass shape:

// Simplified normal calculation from the shader logic
function calculateNormal(uv, cornerRadius, dimensions) {
  const toCorner = [
    Math.max(0, Math.abs(uv[0]) - (dimensions.width / 2 - cornerRadius)),
    Math.max(0, Math.abs(uv[1]) - (dimensions.height / 2 - cornerRadius))
  ];
  
  const distToCorner = Math.sqrt(toCorner[0] ** 2 + toCorner[1] ** 2);
  
  if (distToCorner > 0) {
    // Corner region: radial normal
    return normalize(toCorner);
  } else {
    // Edge region: perpendicular normal
    return [Math.sign(uv[0]), Math.sign(uv[1])];
  }
}

Nested glass hierarchies demonstrate the library's sophisticated rendering model. When you create a child glass element inside a parent container, the child samples from the parent's framebuffer output rather than the original page capture. This creates authentic layered refraction where each glass pane bends light already bent by previous layers:

const parentGlass = new Container({
  element: document.querySelector('#outer-card'),
  shape: 'roundedRect',
  cornerRadius: 32,
  refractionStrength: 0.12
});

const childButton = new Button({
  element: document.querySelector('#inner-button'),
  parent: parentGlass, // Child samples parent's output
  shape: 'pill',
  refractionStrength: 0.18,
  edgeIntensity: 1.0 // Stronger edge for button affordance
});

parentGlass.render();
childButton.render();

Background sampling relies on html2canvas to capture the page content beneath each glass element. On initialization, the library renders the DOM subtree behind your glass container to a texture that shaders can sample from. This approach avoids the complexity of real-time DOM-to-texture conversion but introduces a key architectural trade-off: backgrounds are snapshots, not live. If content behind the glass animates or changes, you must manually call glassCard.updateBackground() to refresh the capture. For static hero sections this is fine; for dynamic interfaces it requires careful lifecycle management.

The Gaussian blur implementation uses a two-pass separable kernel to maintain 60fps on typical hardware. Rather than sampling a full 13×13 grid (169 texture reads per pixel), it performs a 13-tap horizontal pass followed by a 13-tap vertical pass (26 total reads). The kernel weights follow a Gaussian distribution with sigma derived from the blurRadius parameter, providing quality blur without the O(n²) cost of naive approaches. Dynamic uniform updates let you animate blur radius or refraction strength without recreating WebGL contexts, enabling hover effects or scroll-driven glass intensity changes.

Gotcha

The WebGL 2.0 requirement is non-negotiable and immediately excludes a significant chunk of users. Safari only gained WebGL 2.0 support in version 14 (September 2020), meaning any iPhone running iOS 13 or earlier won't render your glass effects at all—they'll see either nothing or whatever fallback you implement. If your analytics show meaningful traffic from older mobile devices, particularly in regions with slower device upgrade cycles, Liquid Glass JS becomes a risky choice that requires parallel implementation paths.

The html2canvas dependency creates performance bottlenecks that aren't obvious until production. Capturing complex DOM trees with CSS transforms, third-party embeds, or canvas elements can take 200-500ms on mid-range mobile devices. This happens on initialization and every time you call updateBackground(), introducing stutter if you're trying to refresh backgrounds during scrolling or animation. Worse, html2canvas has known rendering inconsistencies with certain CSS features (blend modes, some filters, cross-origin images without CORS), meaning your background capture might not perfectly match what users see. Testing across content variations becomes critical.

Framework integration requires custom wrapper components because the library expects direct DOM element references and manual lifecycle control. In React, you can't just drop <LiquidGlass> into JSX—you need useEffect hooks to initialize containers after mount, cleanup functions to dispose WebGL contexts on unmount, and careful ref management to pass DOM nodes to the constructor. For Vue or Svelte, similar ceremony applies. This isn't insurmountable but adds boilerplate that pure CSS solutions avoid entirely. The library shines for standalone hero sections; it fights you when building reusable component systems.

Verdict

Use if: You're building a modern, design-forward web experience targeting recent browsers (Chrome 80+, Safari 14+) where Apple-inspired glass effects with authentic refraction are a core brand differentiator—think SaaS landing pages, design agency portfolios, or luxury product showcases. The library excels when you need nested glass hierarchies, real-time parameter control for hover effects, or shape-specific refraction that CSS backdrop-filter can't deliver. Ideal for hero sections, modal overlays, or card components where glass is an accent rather than pervasive UI element. Skip if: You need broad compatibility including older iOS devices, are working in performance-critical contexts where WebGL overhead and html2canvas captures introduce unacceptable lag, require dynamic backgrounds that update frequently (html2canvas refresh costs add up quickly), or want seamless React/Vue integration without writing custom wrapper components. For standard glass morphism without physical refraction, CSS backdrop-filter with fallbacks remains the pragmatic choice. For complex 3D glass effects beyond UI elements, Three.js provides more power at the cost of complexity.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/developer-tools/dashersw-liquid-glass-js.svg)](https://starlog.is/api/badge-click/developer-tools/dashersw-liquid-glass-js)