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).
<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.
"use client";
import { useState } from "react";
import { GlassScene, GlassButton, GlassSwitch } from "glassy-liquid/react";
export function Welcome() {
const [on, setOn] = useState(true);
return (
<GlassScene 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)" }}>
<GlassButton variant="prominent">Continue</GlassButton>
<GlassSwitch checked={on} onCheckedChange={setOn} aria-label="Wi-Fi" />
</GlassScene>
);
}<script setup lang="ts">
import { ref } from "vue";
import { GlassScene, GlassButton, GlassSwitch } from "glassy-liquid/vue";
const on = ref(true);
</script>
<template>
<GlassScene 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)">
<GlassButton variant="prominent">Continue</GlassButton>
<GlassSwitch v-model:checked="on" aria-label="Wi-Fi" />
</GlassScene>
</template>If you also write raw <glass-*> tags in a Vue template, mark them as custom elements: isCustomElement: (tag) => tag.startsWith("glass-"). The wrapper components do not need that. Named slots are <template #trigger>, #icon, #trailing, and #shortcut, not the HTML slot attribute. The menu sample on the Components page shows #trigger.
<script>
import { GlassScene, GlassButton, GlassSwitch } from "glassy-liquid/svelte";
let on = true;
</script>
<GlassScene 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)">
<GlassButton variant="prominent">Continue</GlassButton>
<GlassSwitch bind:checked={on} aria-label="Wi-Fi" />
</GlassScene>This hello world compiles in Svelte 4 and Svelte 5. bind:checked, bind:value, and bind:open track the controlled props. The component samples on the Components page use Svelte 5 runes ($state, onclick). In Svelte 4, write let and on:click instead. examples/svelte/src/Svelte4.svelte shows the action API, which works in both.
<script type="module" src="https://cdn.jsdelivr.net/npm/glassy-liquid@0.1/dist/glassy-liquid.min.js"></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>
One ES module registers every element. A runnable copy lives in examples/vanilla-cdn.
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.
<!-- 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.
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();| Option | Type | Default | What it does |
|---|---|---|---|
material | regular | clear | regular | Frosted and adaptive, or transparent over photographs. |
appearance | auto | light | dark | auto | auto follows --glass-appearance, then the page color-scheme. |
radius | number | capsule | circle | 24 | Corner radius in CSS pixels. A component may set its own, such as capsule on a button. |
refraction | number | scales with the surface | Maximum displacement at the rim, in CSS pixels. Small controls bend more. |
bezel | number | 18 | Width of the refracting rim, in CSS pixels. |
blur | number | the material's | Frost before refraction (Gaussian sigma, CSS pixels). |
dispersion | 0 to 1 | 0.25 on scene-gl, else 0 | Chromatic split at the rim. It is free on the WebGL scene path and off where it costs extra passes. |
tint | CSS color | none | A wash of color over the transmitted light. |
specular | 0 to 2 | 1 | Strength of the rim light. |
lightAngle | degrees | -135 | Where the light comes from. 0 is the right, -90 is the top. In HTML: light-angle. |
shadow | boolean | CSS | true | Drop shadow under the slab. true uses the material shadow. |
profile | squircle | convex | lip | flat | squircle | Cross-section of the rim. |
zoom | number | 1 | Magnification of the center. A glass() option, not an HTML attribute. |
backdrop | boolean | true | Refract and blur the backdrop. false keeps only light, tint, and shadow. |
fill | CSS color | none | Override the material fill. A glass() option, not an HTML attribute. |
engine | auto | backdrop | scene-gl | scene-dom | css | auto | Force 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.
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.
- examples/vanilla-cdn loads the CDN bundle.
- examples/react uses the React bindings.
- examples/vue uses the Vue 3 bindings.
- examples/svelte uses the Svelte bindings.
Svelte4.svelteshows the action API.