# Liquid Glass

> Build and reuse the "liquid glass" refraction UI in components/grid-wallet-demo (the LiquidGlass / Glass / GlassOver components). Use this skill when the user asks for a glass / liquid-glass / frosted / refraction / squircle UI element — e.g. "make a glass button", "glass tab bar", "glass card", "glass panel", "glass nav", "make this glass", "glass border that expands on hover", "frosted surface", or to tune the glass effect (refraction, chroma, specular, blur, corner smoothing, shadow).

- Skill: `lightsparkdev/liquid-glass` (Agent Skill)
- Install (CLI): `npx skillmds@latest add lightsparkdev/liquid-glass`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lightsparkdev/liquid-glass/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: lightsparkdev (https://skillmd.com/u/lightsparkdev)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lightsparkdev/liquid-glass

---


# Liquid Glass

Reusable refraction glass for `components/grid-wallet-demo`. Full docs:
**`components/grid-wallet-demo/src/components/liquid-glass/README.md`** — read it
before implementing anything non-trivial.

## The rule that prevents 90% of the pain

**The glass refracts its own CHILDREN, not the page behind it.** There is no
"frost whatever is behind me" mode (`backdrop-filter` can't refract portably). To
bend a background you put a *copy* of it inside the glass. So:

- Glass over a **known** backdrop (color / gradient / image / pattern) → use
  `GlassOver`, which makes the copy for you.
- Glass over **live UI behind a small surface** (over a *static* screen) →
  achievable: feed the glass a positioned **copy** of that UI. See "Refract live
  UI behind a small surface" in the gotchas / README.
- Glass over **arbitrary scrolling/animating DOM** → not achievable. Tell the user.

Exception: a **big** frosted surface (sheet/modal) should NOT refract — the
per-frame SVG filter tanks Safari. Use `FrostPanel` (GPU `backdrop-filter` + a
specular edge). Refraction is for the small, tactile elements.

Skipping this gives a flat panel that doesn't lens (the classic failure).

## Pick the right entry point

| Need | Use |
|---|---|
| Glass button / tab bar / card / chip over a known bg | `GlassOver` (easiest) |
| Refract content you already render, or custom backdrop wiring | `Glass` (= `LiquidGlass`) |
| A big frosted sheet/modal (no refraction — fast on Safari) | `FrostPanel` (`BottomSheet` uses it) |
| A small surface that refracts the (static) screen behind it | `GlassOver`'s `backdropNode` |
| A lens over the LIVE aurora that must work on Safari | WebGL: `AuroraLensButton` (circle) / `AuroraLensPanel` (rect squircle) |
| The phone preview's WebGL stage | `glass-gl/StageGL` — **bespoke, don't reuse** |

**SVG vs shader (which renderer):** **default to the SVG path** (`Glass` /
`GlassOver`) for anything made of **live DOM** — buttons, tab bars, cards, text,
layouts (keeps content selectable/clickable). The **shader / WebGL** path is only
needed when you're refracting content that is **not** live DOM — a `<canvas>`
drawing (e.g. a generated QR) or a `<video>` (Safari won't SVG-filter those).
There is **no auto-switching** and no general "glass over a canvas/video" yet:
the only WebGL renderer (`StageGL`) is bespoke to the phone and re-derives the
math in GLSL. If a canvas/video glass actually comes up, **generalize `StageGL`
(or build a map-fed WebGL glass) on demand** — don't assume a drop-in exists.

## Minimal usage

```tsx
import { GlassOver } from '@/components/liquid-glass';

const BG = 'linear-gradient(135deg, #5b9dff, #b07cff)';

<div style={{ background: BG }}>
  <GlassOver backdrop={BG} radius={16} depth={10} scale={18} cornerSmoothing={0.6}>
    <span style={{ display: 'block', padding: '12px 22px', color: '#fff', fontWeight: 600 }}>
      Settings
    </span>
  </GlassOver>
</div>
```

`backdrop` = the same CSS background the surface sits on (gets refracted).
`children` = content on top (not refracted). Any `GlassConfig` field is a prop.
Start from a preset (`DEFAULT_GLASS`, `PHONE_SHELL_GLASS`) and override. Frosted
surfaces (`FrostPanel`) use the slimmer `FrostConfig`, not `GlassConfig`.

## Must-know gotchas (details in the README)

- **Hover/resize:** animate `transform: scale()`, never width/height/radius — the
  displacement map re-bakes on shape change (jank).
- **Refraction needs texture:** over a flat color you only see specular/edge rim.
- **Tiny elements:** lower `depth`/`scale` or they over-distort.
- **`specularRotation`** is directional (45° puts a bright highlight on the
  top-left/bottom-right corners) — rotate or lower `edgeStrength` to move it.
- **`corner-shape: squircle`** is Chromium-only (graceful circular fallback).
- **Outer drop shadow → round the wrapper.** A glass surface clips itself with
  `overflow: hidden`, so an outer `box-shadow` has to live on a wrapper *around*
  the glass (inside, the clip eats it). `box-shadow` traces that wrapper's *own*
  `border-radius`/`corner-shape`, not the glass child's — so an unrounded wrapper
  gives a **square halo around round glass**. Give the shadow wrapper the same
  `border-radius` + `corner-shape` as the glass (see `GlassSymbolButton` /
  `GlassWindowButtonGroup` `.root`). Same rule for any border/background painted
  on a wrapper. Debug heuristic: a square around glass = a rounding-unaware layer.
- **Tiled backgrounds:** pass `GlassOver`'s `backdropOffset={{x,y}}` to align.
- **Circles & pills auto-lens radially** (Aave's production circle/pill model,
  detected from the geometry — no flag). `splay` doesn't apply to them; the dome
  applies to circles only. Don't fight it with per-axis expectations.
- **Live/changing content inside a lens is a Safari minefield** — stale
  filter-output caching, composited descendants escaping the lens unfiltered
  (`translateZ(0)`/`will-change`/motion's `filter: blur(0px)` leftovers),
  canvas/video killing the filter, and a LAYOUT-bounds source-size ceiling. Read
  "Safari field notes" in the README before attempting; the full experiment is
  parked on `pat/scroll-refraction-experiment`.
- **WebKit renders SVG displacement over a copied subtree as EMPTY output** at
  notification size (confirmed June 2026 — see the README's "Confirmed dead
  end" postmortem). Don't retry it: use the WebGL lenses for Safari refraction
  and gate the SVG path to Chromium. Squircle-rect corner rules for Safari
  (map smoothing with `cornerRadii`, overlay border-radius intersection,
  one-element frost, no drop-shadow wrappers) are in the same section.
- **Verifying a bend over a soft backdrop:** displacement is invisible without
  texture — draw a sharp debug grid through the bent coordinates to check.
- **Big sheets/modals don't refract — they frost.** A large displacement lens
  re-runs its whole SVG filter every frame while the surface animates, which tanks
  Safari (CPU `feDisplacementMap`). Use `FrostPanel` (GPU `backdrop-filter` + a
  static specular edge); keep refractive `Glass`/`GlassOver` for the small buttons
  on top. Example: `apps/shared/BottomSheet` (FrostPanel) → `aurora/PasskeySheet`.
- **Refract live UI behind a *small* surface:** pass a *copy* of the behind-UI as
  `GlassOver`'s `backdropNode`, anchored to align, and **counter-animate it**
  against any slide so it stays put while the glass slides over it. Behind-UI must
  be static while open. (Not for big sheets — see above.)

## When tuning

There's a live tuning panel: run the demo (`npm run dev`, port 4000), open the
App panel, switch it to **swag** mode. Sliders map 1:1 to `GlassConfig`.

