@dazecoop/liquid-glass 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +256 -2
- package/lib/index.cjs +1172 -0
- package/lib/index.d.cts +156 -0
- package/lib/index.d.ts +156 -0
- package/lib/index.js +1172 -0
- package/lib/liquid-glass.css +390 -0
- package/lib/liquid-glass.global.js +12 -0
- package/lib/react.cjs +238 -0
- package/lib/react.d.cts +91 -0
- package/lib/react.d.ts +91 -0
- package/lib/react.js +238 -0
- package/package.json +81 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 David Cooper
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,257 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Liquid Glass
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A "liquid glass" lens that refracts, frosts and splits the light of real HTML behind it: text, buttons, inputs, images. Move your cursor (or drag on touch) and the lens follows with spring physics, stretching as it moves. Everything under it stays selectable and clickable.
|
|
4
|
+
|
|
5
|
+
Built with vanilla JavaScript, SVG filters, [Vite](https://vite.dev) and [Tailwind CSS](https://tailwindcss.com).
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
> [!NOTE]
|
|
10
|
+
> Live demo: [glass.dazemx.com](https://glass.dazemx.com/)
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- Rounded-rectangle lens with a curved, refracting rim, over live HTML
|
|
15
|
+
- Frosted blur, chromatic dispersion, brightness and saturation, and a soft drop shadow
|
|
16
|
+
- Lighting: rim light, a Blinn-Phong specular highlight and a fresnel edge glow
|
|
17
|
+
- Adjustable bevel depth, from a thin rim to a full magnifying dome
|
|
18
|
+
- Five presets: Clear, Frosted, Dark, Prism and Dome
|
|
19
|
+
- Glass can also sit behind an element as its background (the demo's dock), and button mode makes it brighten on hover and flatten on press
|
|
20
|
+
- Spring-driven movement with squash and stretch, plus a press effect
|
|
21
|
+
- Press <kbd>L</kbd> to lock the lens in place (handy for screenshots)
|
|
22
|
+
- Live controls for shape, glass and light, grouped in tabs
|
|
23
|
+
- Built-in backgrounds, or upload / drag and drop your own image
|
|
24
|
+
- The lens pops like a bubble when you scroll away from the hero, and swells back in when you return
|
|
25
|
+
- A code section that generates a drop-in snippet with your current settings
|
|
26
|
+
- Retina aware, touch friendly, keyboard nudging, respects `prefers-reduced-motion`
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
`backdrop-filter: url(#svg-filter)` would refract whatever is behind an element, but only Chrome supports it. Instead, the lens holds a copy of the page, lined up with the original, and bends that copy with an SVG displacement filter. Plain `filter: url()` works in every browser. The displacement map and rim lighting are generated from a signed-distance rounded rectangle with a circular bevel.
|
|
31
|
+
|
|
32
|
+
The copy refreshes whenever the page changes (a `MutationObserver` plus input and scroll events). It is `inert` and `aria-hidden`, so it doesn't affect find-in-page, focus or screen readers.
|
|
33
|
+
|
|
34
|
+
Safari needs two workarounds, both handled in [`src/glass.js`](src/glass.js):
|
|
35
|
+
|
|
36
|
+
- It drops an SVG filter when the filtered content contains GPU-composited layers, so the copy has `backdrop-filter`, `will-change` and animations stripped.
|
|
37
|
+
- It scales transformed content inside an SVG filter wrongly on retina screens, so the copy is positioned with `left`/`top`, and zoom scales the filtered layer from outside.
|
|
38
|
+
|
|
39
|
+
Two details keep the refraction steady as the lens moves:
|
|
40
|
+
|
|
41
|
+
- Zoom isn't stored in the displacement map. Its channels are 8-bit, so a zoom stored there moved in half-pixel steps that swept across the content. The filtered layer is built at `1 / zoom` size and scaled up instead, so the map only holds the rim.
|
|
42
|
+
- The lens snaps to whole CSS pixels. Browsers rasterise filters on the CSS pixel grid, so at a fractional position the refraction is resampled and jumps against the settled frame.
|
|
43
|
+
|
|
44
|
+
The demo page has a longer, illustrated walkthrough in its "How it works" section.
|
|
45
|
+
|
|
46
|
+
## Browser support
|
|
47
|
+
|
|
48
|
+
Chrome, Firefox and Safari, on desktop and mobile.
|
|
49
|
+
|
|
50
|
+
Limitations of the copy: it doesn't show hover or focus states, `backdrop-filter` blur inside it is dropped (see above), and WebGL canvases and `<video>` aren't copied. IDs are removed from the copy so they stay unique, so style content inside the source with classes rather than `#id` selectors.
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
npm install @dazecoop/liquid-glass
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
One package with three ways in. The core has no dependencies, and React is an optional peer.
|
|
59
|
+
|
|
60
|
+
| Import | What it is |
|
|
61
|
+
| --- | --- |
|
|
62
|
+
| `@dazecoop/liquid-glass` | `LiquidGlass`, plus the `GlassSwitch`, `GlassTabBar` and `GlassButton` components (ESM and CommonJS) |
|
|
63
|
+
| `@dazecoop/liquid-glass/react` | React components and a `useLiquidGlass` hook |
|
|
64
|
+
| `@dazecoop/liquid-glass/style.css` | Styles for the UI components (the lens itself needs none) |
|
|
65
|
+
|
|
66
|
+
TypeScript types are included.
|
|
67
|
+
|
|
68
|
+
### React
|
|
69
|
+
|
|
70
|
+
```jsx
|
|
71
|
+
import { GlassScene, LiquidGlass } from "@dazecoop/liquid-glass/react";
|
|
72
|
+
|
|
73
|
+
export function Hero() {
|
|
74
|
+
return (
|
|
75
|
+
// The lens follows the pointer over everything inside, refracting it
|
|
76
|
+
<GlassScene glass={{ ...LiquidGlass.presets.frosted, width: 240 }} style={{ height: 480 }}>
|
|
77
|
+
<h1>Liquid Glass</h1>
|
|
78
|
+
</GlassScene>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Every component builds its glass after mount and destroys it on unmount, so StrictMode, route changes and server rendering are all fine. The file is marked `"use client"` for the Next.js app router.
|
|
84
|
+
|
|
85
|
+
| Component | Description |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `<GlassScene glass>` | A lens that follows the pointer over its children, clipped to the scene |
|
|
88
|
+
| `<GlassPanel source button glass>` | An element with glass as its background, refracting `source` (a ref; its parent by default) |
|
|
89
|
+
| `<GlassSwitch checked onChange>` | The switch. Controlled, or uncontrolled with `defaultChecked` |
|
|
90
|
+
| `<GlassSegment options value onChange>` | A switch with a label each side: two `options`, as strings or `{ value, label }` |
|
|
91
|
+
| `<GlassTabBar items value onChange>` | The floating tab bar. `items` are `{ value, label, icon?, href? }` |
|
|
92
|
+
| `<GlassButton tint source>` | Clear glass that lights up in `tint`. Refracts its parent's `[data-glass-source]` child, or `source` |
|
|
93
|
+
| `useLiquidGlass(sourceRef, options)` | Returns the `LiquidGlass` instance for anything custom. Pass `anchor` or `container` as refs |
|
|
94
|
+
|
|
95
|
+
Components take a `look` prop to override their glass parameters, and pass other props (`className`, `style`, `aria-*`) to their element.
|
|
96
|
+
|
|
97
|
+
### JavaScript
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
import { LiquidGlass } from "@dazecoop/liquid-glass";
|
|
101
|
+
|
|
102
|
+
// The lens refracts everything inside this element
|
|
103
|
+
const scene = document.querySelector("main");
|
|
104
|
+
const glass = new LiquidGlass(scene, { container: scene });
|
|
105
|
+
|
|
106
|
+
glass.set({ width: 280, height: 180, radius: 64 });
|
|
107
|
+
|
|
108
|
+
scene.addEventListener("pointermove", (e) => glass.moveTo(e.clientX, e.clientY));
|
|
109
|
+
scene.addEventListener("pointerdown", () => glass.press(true));
|
|
110
|
+
scene.addEventListener("pointerup", () => glass.press(false));
|
|
111
|
+
|
|
112
|
+
// When it's no longer needed
|
|
113
|
+
glass.destroy();
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
To give an element a glass background instead, anchor the glass to it. Add `button: true` to make it respond to hover and press:
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
new LiquidGlass(scene, { anchor: document.querySelector("#panel"), refraction: 12, chroma: 0 });
|
|
120
|
+
new LiquidGlass(scene, { anchor: document.querySelector("#play"), button: true });
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Start from a preset and override what you like:
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
const glass = new LiquidGlass(scene, { ...LiquidGlass.presets.frosted, frost: 8 });
|
|
127
|
+
glass.set(LiquidGlass.presets.dark);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Script tag
|
|
131
|
+
|
|
132
|
+
No install and no build step. The script defines one global, `LiquidGlass`:
|
|
133
|
+
|
|
134
|
+
```html
|
|
135
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@dazecoop/liquid-glass@0/lib/liquid-glass.css" />
|
|
136
|
+
<script src="https://cdn.jsdelivr.net/npm/@dazecoop/liquid-glass@0/lib/liquid-glass.global.js"></script>
|
|
137
|
+
<script>
|
|
138
|
+
const glass = new LiquidGlass(document.querySelector("main"));
|
|
139
|
+
LiquidGlass.initComponents(); // any switches, tab bars and buttons in the page
|
|
140
|
+
</script>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
You can also tune the lens on the [demo](https://glass.dazemx.com/) and copy the code from its **Use it anywhere** section, written for React, JavaScript, a script tag, or as one self-contained file.
|
|
144
|
+
|
|
145
|
+
## UI components
|
|
146
|
+
|
|
147
|
+
A switch, a segmented switch, a floating tab bar and a glass button, after iOS 26. Import `style.css` once. In React, use the components above. In HTML, write the markup and call `initComponents()`:
|
|
148
|
+
|
|
149
|
+
```html
|
|
150
|
+
<button type="button" role="switch" aria-checked="true" aria-label="Wi-Fi" class="lg-switch" data-glass-switch>
|
|
151
|
+
<span class="refract"></span><span class="thumb"></span><span data-opt></span><span data-opt></span>
|
|
152
|
+
</button>
|
|
153
|
+
|
|
154
|
+
<div role="radiogroup" aria-label="Billing" class="lg-segment" data-glass-switch>
|
|
155
|
+
<span class="refract"></span><span class="thumb"></span>
|
|
156
|
+
<button type="button" role="radio" aria-checked="true" data-opt>Monthly</button>
|
|
157
|
+
<button type="button" role="radio" aria-checked="false" data-opt>Yearly</button>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<nav class="lg-tabs" aria-label="Main" data-glass-tabs>
|
|
161
|
+
<span class="lg-pill" hidden></span>
|
|
162
|
+
<span class="lg-glowbox"><span class="lg-glow"></span></span>
|
|
163
|
+
<button type="button" aria-current="page">Home</button>
|
|
164
|
+
<button type="button">Search</button>
|
|
165
|
+
</nav>
|
|
166
|
+
|
|
167
|
+
<!-- The button's lens lives in its (positioned) parent and refracts the [data-glass-source] -->
|
|
168
|
+
<div style="position:relative">
|
|
169
|
+
<div data-glass-source><img src="map.jpg" alt="" /></div>
|
|
170
|
+
<button type="button" class="lg-button" data-glass-button><span class="lg-content">Add stop</span></button>
|
|
171
|
+
</div>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
import { initComponents } from "@dazecoop/liquid-glass";
|
|
176
|
+
|
|
177
|
+
const components = initComponents(); // or initComponents(someElement)
|
|
178
|
+
// Later: components.forEach((c) => c.destroy());
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Switches and tab bars fire a bubbling `change` event (`event.detail.value` is 0 or 1 for a switch, `event.detail.tab` is the element for a tab bar).
|
|
182
|
+
|
|
183
|
+
The dark look is the default. Put `data-theme="light"` on any ancestor for the light one. Every colour is a custom property (`--lg-accent`, `--lg-on`, `--lg-surface` and so on, listed at the top of [`src/liquid-glass.css`](src/liquid-glass.css)), so a theme is a handful of overrides. `--lg-surface` should match the colour right behind a component.
|
|
184
|
+
|
|
185
|
+
## API
|
|
186
|
+
|
|
187
|
+
| Method | Description |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| `new LiquidGlass(source, options)` | Creates a lens over `source`. Options: `anchor` (an element to fill as its background), `container` (an element for the lens to live in, clipped to it), `button: true` (anchored glass responds to hover and press), `throttle` (re-copy a large source at most every this many ms), plus any parameters below. |
|
|
190
|
+
| `LiquidGlass.presets` | `clear`, `frosted`, `dark`, `prism` and `dome`, each a set of parameters. |
|
|
191
|
+
| `set(params)` | Updates any of the parameters below. |
|
|
192
|
+
| `moveTo(x, y)` | Moves the lens to a point in viewport (client) coordinates. |
|
|
193
|
+
| `press(down)` | Grows the lens slightly while `true`. |
|
|
194
|
+
| `show(visible, { pop })` | Fades the lens in or out. With `pop: true` it bursts like a bubble on the way out and swells back in. `moveTo` shows it again. |
|
|
195
|
+
| `destroy()` | Removes the glass and everything it added to the page, and stops its listeners and observers. Safe to call twice. The components have `destroy()` too. |
|
|
196
|
+
|
|
197
|
+
| Parameter | Default | Description |
|
|
198
|
+
| --- | --- | --- |
|
|
199
|
+
| `width`, `height` | `280`, `180` | Lens size in CSS pixels (anchored glass takes its anchor's size) |
|
|
200
|
+
| `radius` | `64` | Corner radius in CSS pixels (anchored glass takes its anchor's) |
|
|
201
|
+
| `bevel` | `35` | How far the curved rim reaches in, in CSS pixels. At half the lens's smaller side or more, it's a full dome |
|
|
202
|
+
| `refraction` | `40` | How far the rim bends the content, in CSS pixels |
|
|
203
|
+
| `frost` | `2` | Blur radius in CSS pixels |
|
|
204
|
+
| `chroma` | `0.4` | Chromatic dispersion at the rim, `0` to `1` |
|
|
205
|
+
| `zoom` | `1.08` | Magnification across the whole lens |
|
|
206
|
+
| `specular` | `0.35` | Strength of the specular highlight, `0` to `1` |
|
|
207
|
+
| `fresnel` | `0.3` | Strength of the edge glow where the glass turns away, `0` to `1` |
|
|
208
|
+
| `brightness` | `0` | Brightness of what's seen through the glass, `-0.5` to `0.5` |
|
|
209
|
+
| `saturation` | `0` | Saturation of what's seen through the glass, `-1` (grey) to `1` |
|
|
210
|
+
| `tint` | `""` | A CSS colour laid over the glass, for legibility |
|
|
211
|
+
| `rim` | `1` | Strength of the rim lighting and edge hairline, `0` to `1` |
|
|
212
|
+
|
|
213
|
+
## Development
|
|
214
|
+
|
|
215
|
+
```sh
|
|
216
|
+
bun install
|
|
217
|
+
bun run dev
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Then open the URL Vite prints. `bun run build` outputs the demo site to `dist/`, and `bun run build:lib` outputs the package to `lib/`.
|
|
221
|
+
|
|
222
|
+
npm works too: `npm install && npm run dev`.
|
|
223
|
+
|
|
224
|
+
## Project structure
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
index.html Demo page markup (Tailwind)
|
|
228
|
+
src/glass.js LiquidGlass: the copy, SVG filter, displacement map and spring physics
|
|
229
|
+
src/components.js GlassSwitch, GlassTabBar and GlassButton
|
|
230
|
+
src/liquid-glass.css The components' styles (the package's style.css)
|
|
231
|
+
src/react.jsx React bindings
|
|
232
|
+
src/index.js Package entry; src/global.js is the script tag's
|
|
233
|
+
types/ TypeScript definitions
|
|
234
|
+
src/main.js Demo UI: backgrounds, presets, sliders, pointer handling, page sections
|
|
235
|
+
src/developer.js The demo's code guide and snippet generator
|
|
236
|
+
src/style.css Demo styles (Tailwind)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Credits
|
|
240
|
+
|
|
241
|
+
Inspired by [this CodePen by shingidad](https://codepen.io/shingidad/pen/jEPxMgW), which this project started as a rebuild of.
|
|
242
|
+
|
|
243
|
+
Lighting, tone, presets, the dome bevel and button mode are adapted from [ybouane/liquidglass](https://github.com/ybouane/liquidglass).
|
|
244
|
+
|
|
245
|
+
Background photos from [Pexels](https://www.pexels.com).
|
|
246
|
+
|
|
247
|
+
Not affiliated with Apple.
|
|
248
|
+
|
|
249
|
+
## Support
|
|
250
|
+
|
|
251
|
+
If my projects have been useful to you and you'd like to support the work, you can buy me a coffee. Completely optional, but always appreciated!
|
|
252
|
+
|
|
253
|
+
<a href="https://www.buymeacoffee.com/dazecoop" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee" style="height: 60px !important;width: 217px !important;" ></a>
|
|
254
|
+
|
|
255
|
+
## Licence
|
|
256
|
+
|
|
257
|
+
MIT. See [LICENSE](LICENSE).
|