{"id":49088145,"url":"https://github.com/ybouane/liquidglass","last_synced_at":"2026-06-08T13:30:29.509Z","repository":{"id":350381133,"uuid":"1200999945","full_name":"ybouane/liquidglass","owner":"ybouane","description":"A liquid glass effect library for the web. Apply realistic glass refraction, blur, chromatic aberration, and lighting effects to any HTML element using WebGL shaders.","archived":false,"fork":false,"pushed_at":"2026-04-10T15:50:17.000Z","size":19129,"stargazers_count":148,"open_issues_count":1,"forks_count":8,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-06T23:05:01.939Z","etag":null,"topics":["chromatic-aberration","effect","filter","glass","glassmorphism","ios26-liquid-glass","liquid","refraction","ui-design","ui-ux","webgl"],"latest_commit_sha":null,"homepage":"https://liquid-glass.ybouane.com/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ybouane.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-04T04:40:16.000Z","updated_at":"2026-05-06T12:13:19.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ybouane/liquidglass","commit_stats":null,"previous_names":["ybouane/liquidglass"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ybouane/liquidglass","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ybouane%2Fliquidglass","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ybouane%2Fliquidglass/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ybouane%2Fliquidglass/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ybouane%2Fliquidglass/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ybouane","download_url":"https://codeload.github.com/ybouane/liquidglass/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ybouane%2Fliquidglass/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34065345,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-08T02:00:07.615Z","response_time":111,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["chromatic-aberration","effect","filter","glass","glassmorphism","ios26-liquid-glass","liquid","refraction","ui-design","ui-ux","webgl"],"created_at":"2026-04-20T17:00:36.922Z","updated_at":"2026-06-08T13:30:29.503Z","avatar_url":"https://github.com/ybouane.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"# LiquidGlass\n\n[![LiquidGlass Banner](https://liquid-glass.ybouane.com/banner.jpg)](https://liquid-glass.ybouane.com/)\n\nA liquid glass effect library for the web. **[Live Demo](https://liquid-glass.ybouane.com/)** Apply realistic glass refraction, blur, chromatic aberration, and lighting effects to any HTML element using WebGL shaders.\n\n## Demo\n\n[https://liquid-glass.ybouane.com/](https://liquid-glass.ybouane.com/)\n\n## Installation\n\n```bash\nnpm install @ybouane/liquidglass\n```\n\nOr skip the install and import directly from a CDN:\n\n```html\n\u003cscript type=\"module\"\u003e\n  import { LiquidGlass } from 'https://cdn.jsdelivr.net/npm/@ybouane/liquidglass/dist/index.js';\n\u003c/script\u003e\n```\n\n## Quick Start\n\n```html\n\u003cdiv id=\"root\"\u003e\n  \u003c!-- Background image (sibling of the glass, captured by the shader) --\u003e\n  \u003cimg class=\"bg\" src=\"background.jpg\" alt=\"\"\u003e\n\n  \u003c!-- Static content --\u003e\n  \u003ch1 class=\"title\"\u003eHello World\u003c/h1\u003e\n\n  \u003c!-- Animated content needs data-dynamic so it re-captures every frame --\u003e\n  \u003cdiv class=\"counter\" data-dynamic\u003e0\u003c/div\u003e\n\n  \u003c!-- Glass element --\u003e\n  \u003cdiv class=\"my-glass\"\u003eGlass Panel\u003c/div\u003e\n\u003c/div\u003e\n\n\u003cscript type=\"module\"\u003e\n  import { LiquidGlass } from '@ybouane/liquidglass';\n\n  const glassEl = document.querySelector('.my-glass');\n  glassEl.dataset.config = JSON.stringify({\n    floating: true,\n    blurAmount: 0.25,\n  });\n\n  const instance = await LiquidGlass.init({\n    root: document.querySelector('#root'),\n    glassElements: [glassEl],\n  });\n\n  // Later, to tear down:\n  // instance.destroy();\n\u003c/script\u003e\n```\n\n## How It Works\n\n1. **Non-glass children of the root** are rasterised onto a hidden canvas using `html-to-image` (which clones the subtree, inlines computed styles, and renders via SVG `foreignObject`). Static children are captured once and cached; children with `data-dynamic` (or any `\u003cvideo\u003e`) are re-captured every frame.\n2. **`\u003cimg\u003e`, `\u003ccanvas\u003e`, and `\u003cvideo\u003e`** are drawn directly via `ctx.drawImage` (faster than `html-to-image`, and the only way to capture live video frames).\n3. **Glass elements** receive an injected child `\u003ccanvas\u003e` that displays the WebGL output. For each glass element, the renderer crops the scene at the panel's location, runs an optional Gaussian blur, then runs a fragment shader that applies refraction, chromatic aberration, Fresnel reflection, multi-light specular highlights, an inner-stroke rim, and a drop shadow.\n4. **Layered compositing** writes each rendered glass canvas back to the compositing canvas before the next glass element runs, so a glass element above another sees the lower one in its refraction.\n\n## API\n\n### `LiquidGlass.init(options)`\n\nAsync — creates and starts a LiquidGlass instance. Resolves once the page's webfonts have been pre-fetched and every glass element's content has been pre-captured.\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `root` | `HTMLElement` | *(required)* | The container element. Glass elements must be **direct children** of this element. |\n| `glassElements` | `NodeList \\| HTMLElement[]` | `[]` | Elements to apply the glass effect to. |\n| `defaults` | `Partial\u003cGlassConfig\u003e` | `{}` | Override the default per-element configuration values for this instance. |\n\n**Returns** a `Promise\u003cLiquidGlass\u003e` resolving to the instance, which exposes:\n\n- `.fps: number` — current measured frames-per-second (updated once per second).\n- `.destroy(): void` — stop the render loop, remove injected canvases, restore mutated inline styles, and free WebGL resources.\n- `.markChanged(element?: HTMLElement): void` — manually flag content the library can't observe on its own (see below).\n\nThe library also exports:\n\n- `invalidateFontEmbedCache(): void` — call after dynamically loading new font stylesheets so the next `init()` rebuilds the embedded font cache.\n\n```javascript\nconst instance = await LiquidGlass.init({\n  root: document.querySelector('#root'),\n  glassElements: document.querySelectorAll('.glass'),\n  defaults: {\n    cornerRadius: 24,\n    refraction: 0.8,\n  },\n});\n```\n\n## Per-Element Configuration\n\nConfigure individual glass elements by setting `data-config` to a JSON string:\n\n```javascript\nelement.dataset.config = JSON.stringify({\n  blurAmount: 0.25,\n  floating: true,\n  cornerRadius: 40,\n});\n```\n\nThe library re-reads `data-config` whenever it changes (via a MutationObserver), so you can update it dynamically.\n\n### Available Options\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `blurAmount` | `number` | `0.00` | Background blur strength (0 = sharp, 1 = maximum blur) |\n| `refraction` | `number` | `0.69` | How much the glass bends the image behind it |\n| `chromAberration` | `number` | `0.05` | Chromatic aberration / colour fringing at edges |\n| `edgeHighlight` | `number` | `0.05` | Edge glow / rim lighting intensity |\n| `specular` | `number` | `0.00` | Specular highlight intensity (multi-light Blinn-Phong) |\n| `fresnel` | `number` | `1.00` | Fresnel reflection at grazing angles |\n| `distortion` | `number` | `0.00` | Micro-distortion noise strength |\n| `cornerRadius` | `number` | `65` | Corner radius in CSS pixels |\n| `zRadius` | `number` | `40` | Bevel depth — controls the curvature of the pill's cross-section |\n| `opacity` | `number` | `1.00` | Overall glass panel opacity |\n| `saturation` | `number` | `0.00` | Saturation adjustment (-1 = grayscale, 0 = normal, 1 = vivid) |\n| `tintStrength` | `number` | `0.00` | Cool blue glass tint strength |\n| `brightness` | `number` | `0.00` | Brightness adjustment (-0.5 to 0.5) |\n| `shadowOpacity` | `number` | `0.30` | Drop shadow opacity |\n| `shadowSpread` | `number` | `10` | Drop shadow spread in CSS pixels |\n| `shadowOffsetY` | `number` | `1` | Shadow vertical offset in CSS pixels |\n| `floating` | `boolean` | `false` | Enable drag-to-move via Pointer Events |\n| `button` | `boolean` | `false` | Button mode — hovering brightens the panel; pressing flattens the bevel and deepens the shadow |\n| `bevelMode` | `0 \\| 1` | `0` | `0` = biconvex pill (default). `1` = dome / plano-convex; pair with `cornerRadius === zRadius` for a half-sphere magnifier. |\n\n## Element Attributes\n\n### `data-dynamic`\n\nAdd `data-dynamic` to any **direct child of the root** whose contents change every frame (counters, animated text, charts). Without it, that wrapper is captured once and cached forever.\n\n```html\n\u003cdiv id=\"root\"\u003e\n  \u003cdiv class=\"static-bg\"\u003e...\u003c/div\u003e          \u003c!-- captured once, cached --\u003e\n  \u003cdiv class=\"counter\" data-dynamic\u003e...\u003c/div\u003e \u003c!-- re-captured every frame --\u003e\n  \u003cdiv class=\"glass\"\u003e...\u003c/div\u003e\n\u003c/div\u003e\n```\n\n`data-dynamic` elements are treated as **always dirty by definition** — the library re-rasterises them every frame and re-runs the shader for every glass that overlaps them. Use it sparingly: it's the only thing on the page that defeats the per-element dirty-tracking optimisation.\n\n`\u003cvideo\u003e` elements are auto-detected as dynamic — you don't need to add `data-dynamic` to them.\n\nFor one-shot updates that don't happen every frame, prefer `instance.markChanged()` (see below) — it costs nothing on idle frames.\n\n### `data-config`\n\nJSON string of per-element configuration options (see the table above). Must decode to an object; invalid JSON or non-object values are ignored with a console warning.\n\n## Manually invalidating content: `instance.markChanged()`\n\nThe library auto-detects most things that affect the glass: DOM mutations inside glass subtrees, `data-config` changes, layout shifts, drag, hover/press, window resize, async capture cache landings, and `data-dynamic` / `\u003cvideo\u003e` elements (which are treated as **always dirty by definition** and re-rendered every frame).\n\nWhat it cannot detect:\n\n- A `\u003ccanvas\u003e` whose pixels you just updated via `getContext('2d')` / WebGL.\n- An `\u003cimg\u003e` whose `src` you just swapped via JS.\n- A wrapper whose CSS `background-image` or other paint property you just updated.\n- Anything else that changes visually without firing a DOM mutation the library is watching.\n\nFor these cases, call:\n\n```javascript\nconst instance = await LiquidGlass.init({ root, glassElements });\n\n// You just repainted a wrapper — only glasses overlapping it will re-render.\ninstance.markChanged(myCanvasElement);\n\n// Or invalidate everything on this instance:\ninstance.markChanged();\n```\n\n`markChanged(element)` walks every glass on the instance, finds the ones whose sample rect intersects the element's bounding rect, and marks just those for a re-render on the next frame. Glasses that don't overlap the element keep their cached output and skip the WebGL pipeline entirely.\n\n`markChanged()` with no argument flags every glass — useful as a \"I don't know what changed but please redraw\" escape hatch.\n\nFor elements with `data-dynamic`, calling `markChanged` is harmless but unnecessary — the library already treats them as dirty every frame.\n\n## Stacking \u0026 Z-Index\n\nThe library re-implements the CSS stacking-context spec to decide painting order on the compositing canvas. It recognises the following stacking-context triggers on direct children of the root:\n\n- Non-static `position` (with z-index)\n- Grid/flex item with explicit `z-index`\n- `opacity \u003c 1`\n- `transform`, `filter`, `perspective`, `clip-path`, `mix-blend-mode`, `isolation`, `backdrop-filter`, `mask-image`, `contain: layout|paint|strict|content`\n- `will-change` listing any of the above properties\n\nIf you put an overlay above a background image and the glass shows the bg but not the overlay, you've hit a missing trigger — file an issue with the property name.\n\n## Limitations \u0026 Gotchas\n\n### Structural\n\n- **Glass elements must be direct children of the root.** Nested glass is rejected at init with a console warning. If you need glass inside a wrapper, give the wrapper its own `LiquidGlass.init()` call.\n- **The root itself is never captured.** The shader samples the root's *children*, so any background image, padding, or border on the root is invisible to the glass effect. Put backgrounds in a sibling element *inside* the root.\n- **A `\u003ccanvas\u003e` is injected as the glass element's first child** for shader output. Avoid `:first-child` selectors on glass elements.\n- **Multiple LiquidGlass roots cannot share refraction.** A glass element in one root cannot see what another root's glass elements are rendering — they each have their own compositing canvas.\n- **The shadow halo extends beyond the glass element.** The injected canvas overflows its parent's box and will be clipped by any ancestor with `overflow: hidden`.\n\n### Performance\n\n- **Capturing DOM into a canvas is expensive.** Every non-glass wrapper is rasterised via `html-to-image` (style inlining + SVG-foreignObject decode). Keep wrappers small and shallow.\n- **`data-dynamic` re-captures every frame.** Use it sparingly — only for content that actually changes.\n- **Each LiquidGlass instance opens its own WebGL context.** Browsers cap concurrent contexts (typically 16 system-wide); don't spawn dozens.\n- **Window resize re-captures everything.** Don't drive layout in a tight resize loop.\n- **The render loop short-circuits when nothing is dirty** — a static page with no `\u003cvideo\u003e` and no `data-dynamic` content does almost no work per frame.\n\n### Text \u0026 fonts\n\n- **Webfonts must be loaded before `init()`** and served with CORS-friendly headers. Google Fonts, jsdelivr, and unpkg work out of the box. Webfonts loaded after init will fall back to system fonts inside captured rasters. Call `invalidateFontEmbedCache()` then re-init if you load fonts dynamically.\n- **Cross-origin `\u003cimg\u003e` elements need `crossorigin=\"anonymous\"`.** Tainted canvases break texture upload and disable the glass effect for the entire root.\n\n### API\n\n- **`LiquidGlass.init()` is async.** It resolves only after the font CSS prefetch, glass content pre-capture, and static-content pre-warm have all completed (typically 100–500 ms on a fresh page).\n- **`data-dynamic` only catches direct children of the root.** Live content nested inside a wrapper that lacks `data-dynamic` will not trigger re-captures.\n- **`destroy()` does not restore an element's original `position: static`** if the library overwrote it with `relative`. Re-init on the same elements is fine; exotic external mutation in between is not.\n\n## Browser Support\n\nRequires WebGL 1.0 + Canvas 2D + SVG `foreignObject`. Effectively all evergreen browsers (Chrome, Firefox, Safari, Edge). WebGL context loss is recovered automatically.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fybouane%2Fliquidglass","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fybouane%2Fliquidglass","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fybouane%2Fliquidglass/lists"}