Guide

Start with one import.

The custom elements work in plain HTML. React, Vue, and Svelte wrap those same elements, with the same names and the same events.

Install

npm install glassy-liquid

React, Vue, and Svelte are optional peer dependencies. Install the one you already use. Nothing else is required: no WebGPU, no runtime dependencies.

First surface

Each snippet is the same screen: a button and a switch on a photograph. src accepts any image URL. The one below is a public photograph, so the example works when you paste it. If that image fails, glass-scene hides the broken icon and keeps linear-gradient(160deg, #9bb7c9, #e7eef2).

index.html
<script type="module">
  import "glassy-liquid/elements";
</script>

<glass-scene src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/58/Iceberg_reflection_in_J%C3%B6kuls%C3%A1rl%C3%B3n_%28Unsplash%29.jpg/960px-Iceberg_reflection_in_J%C3%B6kuls%C3%A1rl%C3%B3n_%28Unsplash%29.jpg" style="height: 100vh; background: linear-gradient(160deg, #9bb7c9, #e7eef2)">
  <glass-button variant="prominent">Continue</glass-button>
  <glass-switch checked aria-label="Wi-Fi"></glass-switch>
</glass-scene>

The bare glassy-liquid/elements import needs a bundler such as Vite, or an import map. Without a build step, use the CDN tab.

Next.js and server rendering

The React entry (glassy-liquid/react) is a Client Component: the file starts with "use client". In the Next.js App Router, import GlassScene, GlassButton, and GlassSwitch from a Client Component. The hello world above already has that directive.

The React, Vue, and Svelte bindings render defer-hydration on each element and remove it after hydration. While the attribute is present the element stays inert, so server markup matches the hydrated tree.

Every entry point is safe to import on the server. Custom elements register only in the browser, where customElements exists, so importing glassy-liquid, glassy-liquid/elements, or a component module from code the server evaluates does not throw. glass(), useGlass(), and attachPress() touch the DOM, so call them in an effect or in onMount, as the playground snippets do.

Scenes

Safari drops SVG filters inside backdrop-filter, so it cannot bend arbitrary page content. A scene tells the glass what is behind it: an image, a video, a canvas, or a DOM layer. Chromium keeps the faster backdrop path unless the scene sets force.

scene.html
<!-- Photograph or video: WebGL, GPU only. src is any image URL. -->
<glass-scene src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/58/Iceberg_reflection_in_J%C3%B6kuls%C3%A1rl%C3%B3n_%28Unsplash%29.jpg/960px-Iceberg_reflection_in_J%C3%B6kuls%C3%A1rl%C3%B3n_%28Unsplash%29.jpg" style="background: linear-gradient(160deg, #9bb7c9, #e7eef2)">…glass…</glass-scene>

<!-- Live DOM: one scoped content filter -->
<glass-scene>
  <article glass-backdrop>…your content…</article>
  <glass-surface radius="capsule"></glass-surface>
</glass-scene>

glass()

glass(element, options) turns your own markup into Liquid Glass and returns a controller. The lens on the home page is built this way. The playground prints a ready-to-paste call.

lens.js
import { glass, attachPress } from "glassy-liquid";

const lens = document.querySelector(".lens");
const surface = glass(lens, {
  radius: "circle",
  material: "clear",
  zoom: 1.18,
  refraction: 30,
});
attachPress(lens, surface);

surface.set({ tint: "#0a84ff" });
surface.engine;   // "backdrop" | "scene-gl" | "scene-dom" | "css"
surface.destroy();
OptionTypeDefaultWhat it does
materialregular | clearregularFrosted and adaptive, or transparent over photographs.
appearanceauto | light | darkautoauto follows --glass-appearance, then the page color-scheme.
radiusnumber | capsule | circle24Corner radius in CSS pixels. A component may set its own, such as capsule on a button.
refractionnumberscales with the surfaceMaximum displacement at the rim, in CSS pixels. Small controls bend more.
bezelnumber18Width of the refracting rim, in CSS pixels.
blurnumberthe material'sFrost before refraction (Gaussian sigma, CSS pixels).
dispersion0 to 10.25 on scene-gl, else 0Chromatic split at the rim. It is free on the WebGL scene path and off where it costs extra passes.
tintCSS colornoneA wash of color over the transmitted light.
specular0 to 21Strength of the rim light.
lightAngledegrees-135Where the light comes from. 0 is the right, -90 is the top. In HTML: light-angle.
shadowboolean | CSStrueDrop shadow under the slab. true uses the material shadow.
profilesquircle | convex | lip | flatsquircleCross-section of the rim.
zoomnumber1Magnification of the center. A glass() option, not an HTML attribute.
backdropbooleantrueRefract and blur the backdrop. false keeps only light, tint, and shadow.
fillCSS colornoneOverride the material fill. A glass() option, not an HTML attribute.
engineauto | backdrop | scene-gl | scene-dom | cssautoForce a renderer. auto uses backdrop in Chromium, a scene inside glass-scene elsewhere, and css when neither is available.

React, Vue, and Svelte expose the same options through useGlass. On a custom element, the same names are attributes, except zoom, backdrop, and fill, which exist only on glass(). In HTML the angle is light-angle. interactive is not a glass option. It is a component flag that turns on the press response (attachPress).

Your own element

Extend GlassElement when you want the layer stack, attribute handling, and the press response. This player is defined on this page.

now-playing.js
import { GlassElement } from "glassy-liquid";

class NowPlaying extends GlassElement {
  static glassDefaults = { radius: 30, material: "clear" };
  static interactive = true;
  static extraAttributes = ["title", "artist"];
  static styles = `
    :host { display: flex; gap: 14px; align-items: center; padding: 12px 18px 12px 12px; color: #fff; }
    .art { width: 54px; height: 54px; border-radius: 14px; background: center/cover url(img/silk.webp); }
    b { display: block; font: 600 16px/1.2 system-ui; }
    span { font: 500 14px/1.3 system-ui; opacity: .75; }`;

  render() {
    return `<div class="art"></div><div><b></b><span></span></div>`;
  }

  // Text from attributes goes in with textContent, never into the markup string.
  setup() {
    this.shadow.querySelector("b").textContent = this.getAttribute("title") ?? "";
    this.shadow.querySelector("span").textContent = this.getAttribute("artist") ?? "";
  }
}

customElements.define("now-playing", NowPlaying);

Runnable examples

Each folder under examples/ is a minimal Vite project that depends on this package. From the repository root, npm run examples:build builds all four.