@aberhamm/liquid-glass-react 0.2.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 +38 -0
- package/README.md +627 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +1191 -0
- package/dist/index.d.ts +1191 -0
- package/dist/index.mjs +1314 -0
- package/dist/liquid-glass-react.css +1 -0
- package/dist/native/index.native.d.ts +19 -0
- package/dist/native/index.native.js +19 -0
- package/dist/native/liquid-glass.native.d.ts +65 -0
- package/dist/native/liquid-glass.native.js +200 -0
- package/dist/native/tokens.d.ts +245 -0
- package/dist/native/tokens.js +254 -0
- package/dist/native/types.d.ts +464 -0
- package/dist/native/types.js +9 -0
- package/package.json +122 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew Aberham and the liquid-glass-react contributors
|
|
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.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Attribution
|
|
26
|
+
|
|
27
|
+
@aberhamm/liquid-glass-react is an independent reimplementation of the
|
|
28
|
+
liquid-glass UI technique popularized by the following projects, both
|
|
29
|
+
distributed under the MIT License:
|
|
30
|
+
|
|
31
|
+
- rdev/liquid-glass-react (https://github.com/rdev/liquid-glass-react)
|
|
32
|
+
- shuding/liquid-glass (https://github.com/shuding/liquid-glass)
|
|
33
|
+
|
|
34
|
+
This project draws inspiration from the prop surface, naming, and visual intent
|
|
35
|
+
of those works, but is a clean-room reimplementation: no source code was copied
|
|
36
|
+
from either project. This attribution is provided to acknowledge that lineage
|
|
37
|
+
and to affirm the independent, non-fork nature of this implementation. It does
|
|
38
|
+
not imply endorsement by the original authors.
|
package/README.md
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
# @aberhamm/liquid-glass-react
|
|
2
|
+
|
|
3
|
+
Liquid-glass UI primitives and batteries-included components for React — a
|
|
4
|
+
frosted, refractive "glass" surface driven by `backdrop-filter` and SVG
|
|
5
|
+
displacement filters, with graceful per-engine degradation and zero runtime
|
|
6
|
+
dependencies.
|
|
7
|
+
|
|
8
|
+
> **Independent reimplementation, not a fork.** This library reimplements the
|
|
9
|
+
> liquid-glass technique popularized by
|
|
10
|
+
> [`rdev/liquid-glass-react`](https://github.com/rdev/liquid-glass-react) and
|
|
11
|
+
> [`shuding/liquid-glass`](https://github.com/shuding/liquid-glass). It shares
|
|
12
|
+
> the *prop surface and intent* — not the code. No source was copied; see
|
|
13
|
+
> [`LICENSE`](./LICENSE) and [`docs/PARITY.md`](./docs/PARITY.md).
|
|
14
|
+
|
|
15
|
+
- **Frosted glass by default, everywhere** — a portable `backdrop-filter` surface
|
|
16
|
+
(blur + saturate + inset-shadow glass edge + rim + elastic motion) that renders
|
|
17
|
+
the same in Chrome, Safari/WebKit, and Firefox. This is the default look.
|
|
18
|
+
- **Opt-in live-backdrop refraction (`displacement`)** — Chromium-only SVG
|
|
19
|
+
`feDisplacementMap` composited over the live backdrop via `backdrop-filter:
|
|
20
|
+
url()`, plus chromatic aberration. Silently falls back to frosted on
|
|
21
|
+
Firefox/Safari.
|
|
22
|
+
- **Opt-in cross-browser refraction (`refract` / `size` / `center`)** — applies
|
|
23
|
+
the same SVG displacement as an element `filter: url()` on a *copy* of content,
|
|
24
|
+
so a real refractive lens renders in Chrome, Safari **and** Firefox.
|
|
25
|
+
- **Web + Expo/React Native from one install** — the same `<LiquidGlass>` import
|
|
26
|
+
resolves to a native (`expo-blur`) frosted build under Metro. iOS is the quality
|
|
27
|
+
bar; Android is best-effort.
|
|
28
|
+
- **Identical box geometry across every tier** — degrading never shifts layout.
|
|
29
|
+
- **SSR-safe** — conservative capabilities until mount, no hydration mismatch.
|
|
30
|
+
- **Zero runtime dependencies.** React 18+ is a peer dependency; the Expo peer
|
|
31
|
+
deps are optional and web consumers never install them.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pnpm add @aberhamm/liquid-glass-react
|
|
39
|
+
# or
|
|
40
|
+
npm install @aberhamm/liquid-glass-react
|
|
41
|
+
# or
|
|
42
|
+
yarn add @aberhamm/liquid-glass-react
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`react` and `react-dom` `>=18` are **peer dependencies** — install them in your
|
|
46
|
+
app if you haven't already:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pnpm add react react-dom
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### CSS import
|
|
53
|
+
|
|
54
|
+
The **prebuilt components** (`GlassButton`, `GlassCard`,
|
|
55
|
+
`GlassSegmentedControl`) require their stylesheet, imported **once** anywhere in
|
|
56
|
+
your app (e.g. your root layout or entry):
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import '@aberhamm/liquid-glass-react/styles.css';
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The low-level `<LiquidGlass>` **primitive does not need the stylesheet** — it is
|
|
63
|
+
fully self-contained and works without it.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
### `LiquidGlass` (the primitive)
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { LiquidGlass } from '@aberhamm/liquid-glass-react';
|
|
73
|
+
|
|
74
|
+
export function Badge() {
|
|
75
|
+
return (
|
|
76
|
+
<LiquidGlass cornerRadius={24} padding="16px 24px">
|
|
77
|
+
<span style={{ color: 'white', fontWeight: 600 }}>Liquid glass</span>
|
|
78
|
+
</LiquidGlass>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `GlassButton`
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { GlassButton } from '@aberhamm/liquid-glass-react';
|
|
87
|
+
import '@aberhamm/liquid-glass-react/styles.css';
|
|
88
|
+
|
|
89
|
+
export function Actions() {
|
|
90
|
+
return (
|
|
91
|
+
<GlassButton variant="primary" size="md" onClick={() => alert('clicked')}>
|
|
92
|
+
Get started
|
|
93
|
+
</GlassButton>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### `GlassCard`
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import { GlassCard } from '@aberhamm/liquid-glass-react';
|
|
102
|
+
import '@aberhamm/liquid-glass-react/styles.css';
|
|
103
|
+
|
|
104
|
+
export function Panel() {
|
|
105
|
+
return (
|
|
106
|
+
<GlassCard elevation="floating">
|
|
107
|
+
<h3>Frosted panel</h3>
|
|
108
|
+
<p>Content stays legible over busy backgrounds.</p>
|
|
109
|
+
</GlassCard>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### `GlassSegmentedControl`
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import { useState } from 'react';
|
|
118
|
+
import { GlassSegmentedControl } from '@aberhamm/liquid-glass-react';
|
|
119
|
+
import '@aberhamm/liquid-glass-react/styles.css';
|
|
120
|
+
|
|
121
|
+
export function ViewToggle() {
|
|
122
|
+
const [value, setValue] = useState('grid');
|
|
123
|
+
return (
|
|
124
|
+
<GlassSegmentedControl
|
|
125
|
+
label="View mode"
|
|
126
|
+
value={value}
|
|
127
|
+
onValueChange={setValue}
|
|
128
|
+
options={[
|
|
129
|
+
{ value: 'grid', label: 'Grid' },
|
|
130
|
+
{ value: 'list', label: 'List' },
|
|
131
|
+
{ value: 'map', label: 'Map' },
|
|
132
|
+
]}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The control is a **native radiogroup** (`<fieldset>` + visually-hidden
|
|
139
|
+
`<input type="radio">`), so Arrow/Home/End/Space keyboard navigation and
|
|
140
|
+
screen-reader semantics work for free. Pass `label` (or `aria-label`) for the
|
|
141
|
+
accessible group name; set `showLabel` to render it visually.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Web usage: frosted default, opt-in refraction
|
|
146
|
+
|
|
147
|
+
On the web the glass renders a **portable frosted surface by default** — the same
|
|
148
|
+
`backdrop-filter` blur/saturate look in Chrome, Safari/WebKit, and Firefox. No
|
|
149
|
+
props are required:
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
import { LiquidGlass } from '@aberhamm/liquid-glass-react';
|
|
153
|
+
|
|
154
|
+
<LiquidGlass cornerRadius={24} padding="16px 24px">
|
|
155
|
+
<span>Frosted by default — portable across engines</span>
|
|
156
|
+
</LiquidGlass>;
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
There are **two independent, opt-in ways** to add real refraction. They compose,
|
|
160
|
+
and both are off by default.
|
|
161
|
+
|
|
162
|
+
### `displacement` — bend the LIVE page (Chromium only)
|
|
163
|
+
|
|
164
|
+
`displacement` attaches the SVG `feDisplacementMap` as a `backdrop-filter: url()`,
|
|
165
|
+
so it warps the **live content behind** the glass. This only composites in
|
|
166
|
+
Chromium (Chrome, Edge, Brave, Opera); Firefox and Safari/WebKit **silently fall
|
|
167
|
+
back** to the frosted surface.
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
// Chromium bends the real page behind the glass; other engines stay frosted.
|
|
171
|
+
<LiquidGlass displacement>
|
|
172
|
+
<span>Live-backdrop refraction where supported</span>
|
|
173
|
+
</LiquidGlass>;
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
> **Upgrading from `0.1.x`?** Refraction used to be the default look. Add
|
|
177
|
+
> `displacement` to restore the old Chromium live-backdrop bend.
|
|
178
|
+
|
|
179
|
+
### `refract` / `size` / `center` — refract a COPY (cross-browser)
|
|
180
|
+
|
|
181
|
+
The cross-browser path applies the same SVG displacement as an **element**
|
|
182
|
+
`filter: url()` on a **copy** of content. Because it filters an element (not the
|
|
183
|
+
backdrop), a real refractive lens renders in Chrome, Safari **and** Firefox. Two
|
|
184
|
+
modes:
|
|
185
|
+
|
|
186
|
+
- **In-place** — pass `size` and/or `center` to refract a copy of the glass's own
|
|
187
|
+
children, with the crisp children on top:
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
<LiquidGlass size={{ width: 320, height: 200 }} center={{ x: 0.5, y: 0.5 }}>
|
|
191
|
+
<span>Refracts a copy of its own children — all engines</span>
|
|
192
|
+
</LiquidGlass>;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- **Lens over foreign content** — pass `refract={node}` to float a lens over
|
|
196
|
+
content the glass doesn't own; `behind` fills any bleed the copied node doesn't
|
|
197
|
+
cover:
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
<LiquidGlass refract={<img src="/poster.jpg" alt="" />} behind="#0b0b0f">
|
|
201
|
+
<span>Floats a refractive lens over the poster</span>
|
|
202
|
+
</LiquidGlass>;
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
> **Which bends what?** `displacement` bends the **live page** behind the glass
|
|
206
|
+
> (Chromium only). `refract` / `size` / `center` refract a **copy** of content via
|
|
207
|
+
> an element filter (Chrome, Safari, **and** Firefox). They are distinct and
|
|
208
|
+
> compose. The refracted copy is decorative — `aria-hidden` + `inert` +
|
|
209
|
+
> `pointer-events: none` — so keep the copied subtree **purely presentational**
|
|
210
|
+
> (no `id`s, effects, focusable controls, or data fetching; it mounts twice).
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Expo / React Native
|
|
215
|
+
|
|
216
|
+
The **same `<LiquidGlass>` import** works in an Expo / React Native app. Metro
|
|
217
|
+
resolves the native build automatically via the package's `react-native` export
|
|
218
|
+
condition — there is no separate import path:
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
import { LiquidGlass } from '@aberhamm/liquid-glass-react';
|
|
222
|
+
import { Text } from 'react-native';
|
|
223
|
+
|
|
224
|
+
export function Badge() {
|
|
225
|
+
return (
|
|
226
|
+
<LiquidGlass cornerRadius={24} padding={16} onPress={() => {}}>
|
|
227
|
+
<Text style={{ color: 'white', fontWeight: '600' }}>Liquid glass</Text>
|
|
228
|
+
</LiquidGlass>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The native surface renders **frosted glass** — an `expo-blur` `BlurView` with a
|
|
234
|
+
tint and a gradient sheen. It is a genuinely different renderer from the web build,
|
|
235
|
+
so the web-only props are **documented no-ops** on native: `displacement`,
|
|
236
|
+
`refract`, `size`, `center`, `behind`, `saturation`, `variant`, `mode`,
|
|
237
|
+
`aberrationIntensity`, and the pointer/motion props. The native primitive uses
|
|
238
|
+
`onPress` (the web primitive uses `onClick`) and takes `style?: ViewStyle`.
|
|
239
|
+
|
|
240
|
+
> **iOS is the quality bar; Android is best-effort.** Android uses expo-blur's
|
|
241
|
+
> `experimentalBlurMethod` and degrades gracefully where blur support is limited.
|
|
242
|
+
|
|
243
|
+
### Optional peer deps
|
|
244
|
+
|
|
245
|
+
The Expo target needs two **optional** peer dependencies — `expo-blur` and
|
|
246
|
+
`expo-linear-gradient` — plus `react-native` (and `expo` itself), which your Expo
|
|
247
|
+
app already provides. They are marked `optional` in `peerDependenciesMeta`, so
|
|
248
|
+
**web-only consumers never install them**:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
npx expo install expo-blur expo-linear-gradient
|
|
252
|
+
# react-native and expo come from your Expo app itself
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The prebuilt `GlassButton`, `GlassCard`, and `GlassSegmentedControl` are
|
|
256
|
+
**web-only this round** — they are DOM components and are intentionally **not**
|
|
257
|
+
part of the native barrel (`src/index.native.ts`), so they won't resolve in a
|
|
258
|
+
native bundle. Use the `<LiquidGlass>` primitive on native.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## What works where
|
|
263
|
+
|
|
264
|
+
| Capability | Chrome / Edge | Safari / WebKit | Firefox | Expo (iOS) | Expo (Android) |
|
|
265
|
+
| --- | --- | --- | --- | --- | --- |
|
|
266
|
+
| **Frosted glass** (default) | ✅ | ✅ | ✅ | ✅ | ⚠️ best-effort |
|
|
267
|
+
| **`displacement`** (live-backdrop bend) | ✅ | frosted fallback | frosted fallback | no-op | no-op |
|
|
268
|
+
| **Cross-browser refraction** (`refract` / `size` / `center`) | ✅ | ✅ | ✅ | no-op | no-op |
|
|
269
|
+
| **Prebuilt** `GlassButton` / `GlassCard` / `GlassSegmentedControl` | ✅ | ✅ | ✅ | ❌ web-only | ❌ web-only |
|
|
270
|
+
|
|
271
|
+
Frosted is the portable default everywhere. `displacement` bends the live page and
|
|
272
|
+
is Chromium-only (frosted fallback elsewhere). The cross-browser `refract` path
|
|
273
|
+
renders a refractive copy in all three web engines. The prebuilt components are
|
|
274
|
+
web-only this round.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## The `asChild` polymorphism pattern
|
|
279
|
+
|
|
280
|
+
`GlassButton` and `GlassCard` accept `asChild`. When set, the component renders
|
|
281
|
+
**your** single child element instead of its default tag (`<button>` / `<div>`),
|
|
282
|
+
merging its props, `className`, and `ref` onto it. The child then owns its own
|
|
283
|
+
semantics and accessibility — ideal for link-styled buttons:
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
import { GlassButton } from '@aberhamm/liquid-glass-react';
|
|
287
|
+
|
|
288
|
+
<GlassButton asChild variant="secondary">
|
|
289
|
+
<a href="/docs">Read the docs</a>
|
|
290
|
+
</GlassButton>;
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Capability detection
|
|
296
|
+
|
|
297
|
+
Use the hook (or the underlying detector) to branch on what the current engine
|
|
298
|
+
can actually render. The single gate is `canRefract` — `true` only when the full
|
|
299
|
+
SVG-displacement refraction will composite (Chromium with `backdrop-filter`):
|
|
300
|
+
|
|
301
|
+
```tsx
|
|
302
|
+
import { useGlassCapabilities } from '@aberhamm/liquid-glass-react';
|
|
303
|
+
|
|
304
|
+
function Hint() {
|
|
305
|
+
const caps = useGlassCapabilities();
|
|
306
|
+
return caps.canRefract
|
|
307
|
+
? <p>Full refraction is active.</p>
|
|
308
|
+
: <p>Showing the frosted fallback for this browser.</p>;
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
During SSR and before the mount effect runs, every capability is conservatively
|
|
313
|
+
`false` (so the server and first client render agree). For one-shot,
|
|
314
|
+
non-reactive checks there is also `detectGlassCapabilities()`:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
import { detectGlassCapabilities } from '@aberhamm/liquid-glass-react';
|
|
318
|
+
|
|
319
|
+
const caps = detectGlassCapabilities(); // GlassCapabilities
|
|
320
|
+
if (caps.canRefract) {
|
|
321
|
+
/* ... */
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
`GlassCapabilities` fields: `supportsBackdropFilter`, `isChromium`,
|
|
326
|
+
`supportsSvgBackdropDisplacement`, `isFirefox`, `prefersReducedMotion`, and the
|
|
327
|
+
derived `canRefract`.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## Browser support
|
|
332
|
+
|
|
333
|
+
The **default** render is the portable frosted surface — identical in Chrome,
|
|
334
|
+
Safari/WebKit, and Firefox. The runtime `canRefract` gate
|
|
335
|
+
(`supportsBackdropFilter && supportsSvgBackdropDisplacement`, where the latter is
|
|
336
|
+
positive Blink-family detection) only decides what the **opt-in `displacement`**
|
|
337
|
+
prop does: whether the live-backdrop SVG bend composites, or degrades to frosted.
|
|
338
|
+
The cross-browser `refract` / `size` / `center` path does **not** depend on this
|
|
339
|
+
gate — it renders in every engine. All tiers share **identical box geometry**, so
|
|
340
|
+
moving between them causes **no layout shift**. See
|
|
341
|
+
[`docs/PARITY.md`](./docs/PARITY.md) for the full contract.
|
|
342
|
+
|
|
343
|
+
Behavior when `displacement` is requested:
|
|
344
|
+
|
|
345
|
+
| Engine | `canRefract` | With `displacement` | Default (no `displacement`) |
|
|
346
|
+
| --- | --- | --- | --- |
|
|
347
|
+
| **Chromium** (Chrome, Edge, Brave, Opera) | `true` | **Live-backdrop refraction** — SVG `feDisplacementMap` bends the live page behind the glass, over `backdrop-filter` blur/saturate, with chromatic aberration and elastic motion. | Frosted surface. |
|
|
348
|
+
| **Firefox** (Gecko) | `false` | **Frosted fallback** — `displacement` degrades silently; no live-backdrop bend. | Frosted surface. |
|
|
349
|
+
| **Safari / WebKit** | `false` | **Frosted fallback** — same as Firefox; WebKit does not composite the SVG displacement over the backdrop. | Frosted surface. |
|
|
350
|
+
| No `backdrop-filter` (very old engines) | `false` | **Solid translucent fallback** — scheme-aware `rgba(...)` fill so content stays legible; never a transparent box. | Solid translucent fallback. |
|
|
351
|
+
| **SSR / pre-mount** | `false` | **Conservative (frosted/degraded)** — all capabilities `false` until the client mount effect re-evaluates, avoiding hydration mismatch. | Conservative. |
|
|
352
|
+
|
|
353
|
+
The cross-browser `refract` / `size` / `center` path renders a refractive **copy**
|
|
354
|
+
in all of the above web engines regardless of `canRefract` (see
|
|
355
|
+
[Web usage](#refract--size--center--refract-a-copy-cross-browser)).
|
|
356
|
+
|
|
357
|
+
Why not feature-detect refraction directly? There is no standardized
|
|
358
|
+
`CSS.supports` probe for "an SVG `feDisplacementMap` composites correctly over
|
|
359
|
+
`backdrop-filter`" — it's a Chromium rendering-pipeline quirk. The library uses
|
|
360
|
+
positive Blink detection (a pragmatic, revisitable heuristic) to keep Safari and
|
|
361
|
+
Firefox in the frosted tier by construction. Details and caveats live in
|
|
362
|
+
[`docs/PARITY.md`](./docs/PARITY.md).
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Displacement `mode`
|
|
367
|
+
|
|
368
|
+
The primitive's `mode` prop selects the displacement algorithm used to generate
|
|
369
|
+
the SVG filter (full effect renders in Chromium; other engines show the frosted
|
|
370
|
+
fallback regardless of `mode`):
|
|
371
|
+
|
|
372
|
+
| Mode | Look |
|
|
373
|
+
| --- | --- |
|
|
374
|
+
| `standard` *(default)* | Balanced, edge-weighted displacement — the default glass look. |
|
|
375
|
+
| `polar` | Radial/polar displacement, stronger toward the perimeter. |
|
|
376
|
+
| `prominent` | Exaggerated displacement for a heavier "thick glass" feel. |
|
|
377
|
+
| `shader` | Shader-style profile for sharper highlights. The displacement map is **generated at runtime** (canvas → `data:` URL feeding an `feImage`). |
|
|
378
|
+
| `turbulence` | Adds fractal `feTurbulence` for an organic, watery, **procedural frosted-ripple** distortion. |
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
<LiquidGlass mode="turbulence">…</LiquidGlass>
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Content-adaptive auto-tint (`adaptiveTint`)
|
|
387
|
+
|
|
388
|
+
Apple's Liquid Glass adapts its tint and content treatment to the brightness of
|
|
389
|
+
whatever is behind it. `adaptiveTint` brings that to `<LiquidGlass>`: when `true`
|
|
390
|
+
the glass samples the luminance of its backdrop and automatically shifts toward a
|
|
391
|
+
**light** or **dark** treatment — the SAME tint / displacement / blur plumbing
|
|
392
|
+
`overLight` already drives — so foreground content (labels, captions) stays
|
|
393
|
+
legible without hand-tuning. A bright backdrop yields the light treatment with
|
|
394
|
+
dark ink; a dark backdrop yields the default treatment with light ink.
|
|
395
|
+
|
|
396
|
+
```tsx
|
|
397
|
+
// No overLight needed — the glass figures out light vs dark for you.
|
|
398
|
+
<LiquidGlass adaptiveTint>
|
|
399
|
+
<span>Adapts to its backdrop</span>
|
|
400
|
+
</LiquidGlass>
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
It is **additive and opt-in**: `adaptiveTint` defaults to `false`, so the default
|
|
404
|
+
render is byte-for-byte unchanged and the luminance sampler is never loaded on the
|
|
405
|
+
default path.
|
|
406
|
+
|
|
407
|
+
**Precedence — `overLight` always wins.** `overLight` is the manual override;
|
|
408
|
+
`adaptiveTint` is the auto path. They never fight. When `overLight` is set
|
|
409
|
+
explicitly it short-circuits the auto path:
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
effectiveOverLight = overLight ?? (adaptiveTint && scheme ? scheme === 'light' : false)
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**SSR / hydration-safe.** The server and the first client paint render the
|
|
416
|
+
default (unsampled) treatment, so hydration never mismatches; the sampled
|
|
417
|
+
treatment is applied in an effect after mount.
|
|
418
|
+
|
|
419
|
+
**Graceful degradation.** When the backdrop can't be sampled — a **cross-origin**
|
|
420
|
+
backdrop taints the canvas, there's no canvas, or it's SSR — the reading is
|
|
421
|
+
`sampled: false` and auto-tint silently falls back to `overLight ?? false`. No
|
|
422
|
+
error, no flicker loop, no `console.error`.
|
|
423
|
+
|
|
424
|
+
**Accessibility.** Under `(prefers-contrast: more)` the increased-contrast
|
|
425
|
+
treatment **wins**: auto-tint never undercuts the high-contrast surface or
|
|
426
|
+
legibility treatment.
|
|
427
|
+
|
|
428
|
+
> ⚠️ **Limitation.** Auto-tint is **best-effort legibility**. Critical text over
|
|
429
|
+
> unknown or cross-origin backdrops (which cannot be sampled) should be verified
|
|
430
|
+
> manually — the auto path falls back rather than guessing.
|
|
431
|
+
|
|
432
|
+
---
|
|
433
|
+
|
|
434
|
+
## Scroll-aware shadow (`scrollAwareShadow`)
|
|
435
|
+
|
|
436
|
+
Apple's Liquid Glass deepens a pinned bar's drop-shadow as content scrolls
|
|
437
|
+
beneath it — lifting it above the text — and eases it over solid backgrounds.
|
|
438
|
+
`scrollAwareShadow` brings that to `<LiquidGlass>`: when `true` the glass reuses
|
|
439
|
+
the **same** backdrop-luminance sampler as `adaptiveTint` and modulates **only**
|
|
440
|
+
its decoupled drop-shadow — **deeper and darker** over a dark/dense backdrop,
|
|
441
|
+
**shallower and lighter** over a light/solid one.
|
|
442
|
+
|
|
443
|
+
```tsx
|
|
444
|
+
// The drop-shadow tracks the backdrop as content scrolls beneath the bar.
|
|
445
|
+
<LiquidGlass scrollAwareShadow>
|
|
446
|
+
<span>Pinned toolbar</span>
|
|
447
|
+
</LiquidGlass>
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
It is **additive and opt-in**: `scrollAwareShadow` defaults to `false`, so the
|
|
451
|
+
default render is byte-for-byte unchanged and the sampler is never loaded on the
|
|
452
|
+
default path. Only the shadow's **blur / offset / opacity** vary — it stays the
|
|
453
|
+
decoupled sibling behind the clipped surface (never a `box-shadow` on the clipped
|
|
454
|
+
node), so no clipping eats it and no new layer is added.
|
|
455
|
+
|
|
456
|
+
**SSR / hydration-safe.** The server and the first client paint render the
|
|
457
|
+
conservative static shadow; the modulated shadow is applied in an effect after
|
|
458
|
+
mount, so hydration never mismatches.
|
|
459
|
+
|
|
460
|
+
**Graceful degradation.** When the backdrop can't be sampled (cross-origin taint,
|
|
461
|
+
no canvas, SSR) the reading is `sampled: false` and the shadow falls back to the
|
|
462
|
+
static one — no error, no flicker loop, no `console.error`.
|
|
463
|
+
|
|
464
|
+
**Reduced motion.** Under `(prefers-reduced-motion: reduce)` the shadow **snaps**
|
|
465
|
+
between depths instead of animating (the `box-shadow` transition is dropped).
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## Material variants: Regular vs Clear (`variant`)
|
|
470
|
+
|
|
471
|
+
Apple distinguishes two Liquid Glass materials, and `variant` lets you pick one
|
|
472
|
+
intentionally instead of hand-tuning opacity:
|
|
473
|
+
|
|
474
|
+
- **`regular`** (default) — the dependable, fully adaptive control surface. This
|
|
475
|
+
is **exactly** today's behavior, including content-adaptive auto-tint when
|
|
476
|
+
[`adaptiveTint`](#content-adaptive-auto-tint-adaptivetint) is on.
|
|
477
|
+
- **`clear`** — a permanently **more transparent** material for media-rich
|
|
478
|
+
contexts (floating over a photo or video). It is **non-adaptive by
|
|
479
|
+
definition**, with a subtle dimming scrim behind the content so labels stay
|
|
480
|
+
legible over busy media.
|
|
481
|
+
|
|
482
|
+
```tsx
|
|
483
|
+
// Dependable control surface (default).
|
|
484
|
+
<LiquidGlass variant="regular">
|
|
485
|
+
<span>Toolbar</span>
|
|
486
|
+
</LiquidGlass>
|
|
487
|
+
|
|
488
|
+
// Maximally transparent over media — clearer, with a legibility scrim.
|
|
489
|
+
<LiquidGlass variant="clear">
|
|
490
|
+
<span>Over a photo</span>
|
|
491
|
+
</LiquidGlass>
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
It is **additive and non-breaking**: `variant` defaults to `'regular'`, so
|
|
495
|
+
omitting it keeps today's render byte-for-byte unchanged. It is a small parameter
|
|
496
|
+
lookup feeding the existing surface/content styles — not a theming system.
|
|
497
|
+
|
|
498
|
+
**Don't mix — Clear is non-adaptive.** Per Apple's guidance the two should never
|
|
499
|
+
be mixed in the same context. In `'clear'`:
|
|
500
|
+
|
|
501
|
+
- **`adaptiveTint` is a no-op** — Clear never samples the backdrop or flips its
|
|
502
|
+
tint/ink. (Use `'regular'` + `adaptiveTint` for the adaptive material.)
|
|
503
|
+
- **`overLight` still nudges legibility**, but does NOT re-enable adaptivity.
|
|
504
|
+
|
|
505
|
+
**Accessibility wins in both variants.** Under `(prefers-contrast: more)` the
|
|
506
|
+
increased-contrast treatment (solid border, opaque fill, pinned saturation)
|
|
507
|
+
applies to Clear too — a11y beats the Clear aesthetic.
|
|
508
|
+
|
|
509
|
+
The prebuilt components (`GlassButton`, `GlassCard`, `GlassSegmentedControl`)
|
|
510
|
+
take `variant` through their `glassProps` escape hatch — no new per-component
|
|
511
|
+
prop:
|
|
512
|
+
|
|
513
|
+
```tsx
|
|
514
|
+
<GlassButton glassProps={{ variant: 'clear' }}>Over media</GlassButton>
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
519
|
+
## API reference
|
|
520
|
+
|
|
521
|
+
### `<LiquidGlass>` (primitive)
|
|
522
|
+
|
|
523
|
+
| Prop | Type | Default | Description |
|
|
524
|
+
| --- | --- | --- | --- |
|
|
525
|
+
| `children` | `ReactNode` | — | Content rendered inside the glass surface. |
|
|
526
|
+
| `displacement` | `boolean` | `false` | Opt into live-backdrop SVG refraction (**Chromium only**; frosted fallback on Firefox/Safari). Bends the real page behind the glass via `backdrop-filter: url()`. No-op on native. |
|
|
527
|
+
| `displacementScale` | `number` | `70` | Strength of the refraction distortion; higher bends the backdrop more. |
|
|
528
|
+
| `blurAmount` | `number` | `0.0625` | Backdrop blur radius (px) applied behind the glass. |
|
|
529
|
+
| `saturation` | `number` | `140` | Backdrop saturation multiplier (`1` = unchanged). |
|
|
530
|
+
| `aberrationIntensity` | `number` | `2` | Chromatic aberration (RGB separation) at refracted edges; `0` disables. |
|
|
531
|
+
| `elasticity` | `number` | `0.15` | Pointer-follow softness; `0` is rigid, higher is rubbery. |
|
|
532
|
+
| `cornerRadius` | `number \| string` | `999` | Corner radius. Number = px; string = CSS length (e.g. `'1rem'`, `'50%'`). |
|
|
533
|
+
| `padding` | `number \| string` | `'24px 32px'` | Inner padding. Number = px; string = CSS shorthand. |
|
|
534
|
+
| `overLight` | `boolean` | `false` | Hint that the glass sits over a light background; tunes tint/contrast. The manual override — always wins over `adaptiveTint`. |
|
|
535
|
+
| `adaptiveTint` | `boolean` | `false` | Opt into content-adaptive auto-tint: samples the backdrop and auto-shifts light/dark for legibility (see [Content-adaptive auto-tint](#content-adaptive-auto-tint-adaptivetint)). |
|
|
536
|
+
| `scrollAwareShadow` | `boolean` | `false` | Opt in: the decoupled drop-shadow deepens/darkens over dark/dense backdrops and eases/lightens over light/solid ones, from the same backdrop sampler (see [Scroll-aware shadow](#scroll-aware-shadow-scrollawareshadow)). |
|
|
537
|
+
| `variant` | `'regular' \| 'clear'` | `'regular'` | Material variant. `regular` = today's fully adaptive surface; `clear` is permanently more transparent and non-adaptive (`adaptiveTint` is a no-op) with a dimming scrim (see [Material variants](#material-variants-regular-vs-clear-variant)). |
|
|
538
|
+
| `mode` | `DisplacementMode` | `'standard'` | Displacement algorithm (see [Displacement `mode`](#displacement-mode)). |
|
|
539
|
+
| `refract` | `ReactNode` | `undefined` | **Cross-browser** copy-refraction: floats a refracted lens rendering this node (Chrome, Safari, Firefox). The copy is `aria-hidden` + `inert` + `pointer-events:none` — keep it presentational. No-op on native. |
|
|
540
|
+
| `size` | `GlassSize` (`{ width: number; height: number }`) | measured | Explicit lens size in px. Setting it (with or without `refract`) opts into **in-place** cross-browser copy-refraction of the glass's own children. No-op on native. |
|
|
541
|
+
| `center` | `GlassCenter` (`{ x: number; y: number }`) | `{ x: 0.5, y: 0.5 }` | Lens focal point as fractions in `[0, 1]`; drives the refracted copy's `transform-origin`. Opts into in-place copy-refraction like `size`. No-op on native. |
|
|
542
|
+
| `behind` | `string` | `undefined` | CSS `background` painted behind the refracted copy to fill bleed (most useful with `refract`). No effect unless copy-refraction is active. No-op on native. |
|
|
543
|
+
| `className` | `string` | — | Class name(s) on the outermost glass element. |
|
|
544
|
+
| `style` | `CSSProperties` | — | Inline styles merged onto the outermost element. |
|
|
545
|
+
| `onClick` | `MouseEventHandler<HTMLDivElement>` | — | Click handler forwarded to the surface. |
|
|
546
|
+
| `globalMousePos` | `MousePos` | uncontrolled | Externally controlled global pointer position (for coordinating many surfaces). |
|
|
547
|
+
| `mouseOffset` | `MousePos` | uncontrolled | Externally controlled pointer offset from the element center. |
|
|
548
|
+
| `mouseContainer` | `RefObject<HTMLElement \| null> \| HTMLElement \| null` | `null` (viewport) | Element whose bounds define the pointer-tracking coordinate space. |
|
|
549
|
+
|
|
550
|
+
### `<GlassButton>`
|
|
551
|
+
|
|
552
|
+
Extends native `<button>` attributes (`onClick`, `disabled`, `type`, `aria-*`,
|
|
553
|
+
`ref`, …) — all are forwarded.
|
|
554
|
+
|
|
555
|
+
| Prop | Type | Default | Description |
|
|
556
|
+
| --- | --- | --- | --- |
|
|
557
|
+
| `variant` | `'primary' \| 'secondary' \| 'subtle'` | `'primary'` | Visual emphasis. |
|
|
558
|
+
| `size` | `'sm' \| 'md' \| 'lg' \| 'icon'` | `'md'` | Sizing; `icon` is square for a single-icon child. |
|
|
559
|
+
| `asChild` | `boolean` | `false` | Render the single child element instead of a `<button>` (see [`asChild`](#the-aschild-polymorphism-pattern)). |
|
|
560
|
+
| `shine` | `boolean` | `true` | Brief highlight sweep on press. |
|
|
561
|
+
| `contentClassName` | `string` | — | Class on the isolated content layer (the span holding children). |
|
|
562
|
+
| `glassProps` | `Partial<Omit<LiquidGlassProps, 'children'>>` | — | Escape hatch: pass-through overrides to the underlying `<LiquidGlass>`. |
|
|
563
|
+
|
|
564
|
+
### `<GlassCard>`
|
|
565
|
+
|
|
566
|
+
Extends native `<div>` attributes (`ref`, `className`, `style`, `children`, …).
|
|
567
|
+
|
|
568
|
+
| Prop | Type | Default | Description |
|
|
569
|
+
| --- | --- | --- | --- |
|
|
570
|
+
| `elevation` | `'flat' \| 'raised' \| 'floating'` | `'raised'` | Ambient lift under the card. |
|
|
571
|
+
| `asChild` | `boolean` | `false` | Render the single child element instead of a `<div>`. |
|
|
572
|
+
| `contentClassName` | `string` | — | Class on the isolated content layer. |
|
|
573
|
+
| `glassProps` | `Partial<Omit<LiquidGlassProps, 'children'>>` | — | Pass-through overrides to the underlying `<LiquidGlass>`. |
|
|
574
|
+
|
|
575
|
+
### `<GlassSegmentedControl>`
|
|
576
|
+
|
|
577
|
+
| Prop | Type | Default | Description |
|
|
578
|
+
| --- | --- | --- | --- |
|
|
579
|
+
| `options` | `GlassSegmentedOption[]` | — | Segments: `{ value, label?, icon?, disabled? }`. Icon-only options are supported. |
|
|
580
|
+
| `value` | `string` | — | Controlled selected value. When set, the control is controlled. |
|
|
581
|
+
| `defaultValue` | `string` | first option | Initial selected value when uncontrolled. |
|
|
582
|
+
| `onValueChange` | `(value: string) => void` | — | Fires on every user selection with the newly-selected value. |
|
|
583
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size variant. |
|
|
584
|
+
| `label` | `ReactNode` | — | Accessible group label (rendered into the `<legend>`). |
|
|
585
|
+
| `aria-label` | `string` | — | Accessible group label as a plain string (alternative to `label`). |
|
|
586
|
+
| `showLabel` | `boolean` | `false` | Render the `label` visually above the control instead of hiding it. |
|
|
587
|
+
| `name` | `string` | generated id | Stable `name` for the radio group. |
|
|
588
|
+
| `className` | `string` | — | Class on the root `<fieldset>`. |
|
|
589
|
+
| `style` | `CSSProperties` | — | Inline styles on the root `<fieldset>`. |
|
|
590
|
+
| `glassProps` | `Partial<Omit<LiquidGlassProps, 'children'>>` | — | Pass-through overrides to the indicator's `<LiquidGlass>`. |
|
|
591
|
+
|
|
592
|
+
### Also exported
|
|
593
|
+
|
|
594
|
+
- **Hooks** — `useGlassCapabilities`, `useReducedMotion`, `useMousePosition`.
|
|
595
|
+
- **Utilities** — `detectGlassCapabilities`, `getConservativeGlassCapabilities`,
|
|
596
|
+
`getDisplacementMap`, `roundedRectSDF`, `smoothStep`,
|
|
597
|
+
`calculateDirectionalScale`, `calculateElasticTranslation`,
|
|
598
|
+
`getGlassEdgeShadow`, and the `GLASS_EDGE_LIGHT` / `GLASS_EDGE_DARK` constants.
|
|
599
|
+
- **Types** — `LiquidGlassProps`, `DisplacementMode`, `GlassVariant`,
|
|
600
|
+
`GlassSize`, `GlassCenter`, `MousePos`, `GlassCapabilities`, `GlassButtonProps`,
|
|
601
|
+
`GlassCardProps`, `GlassSegmentedControlProps`, `GlassSegmentedOption`, and the
|
|
602
|
+
variant/size/elevation unions. The native barrel additionally exports
|
|
603
|
+
`LiquidGlassCoreProps` (the platform-neutral base).
|
|
604
|
+
- **`VERSION`** — the package version string.
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
## Storybook
|
|
609
|
+
|
|
610
|
+
A live showcase (refraction, all five modes, the prebuilt components, and the
|
|
611
|
+
fallback tiers) runs in Storybook:
|
|
612
|
+
|
|
613
|
+
```bash
|
|
614
|
+
pnpm storybook # dev server at http://localhost:6006
|
|
615
|
+
pnpm build-storybook # static build into storybook-static/
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
The **Showcase** story is the headline demo: full refraction in Chromium, the
|
|
619
|
+
frosted fallback in Firefox/Safari.
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
## License
|
|
624
|
+
|
|
625
|
+
MIT — see [`LICENSE`](./LICENSE). The license includes an attribution note
|
|
626
|
+
acknowledging the technique's lineage while affirming this is an independent
|
|
627
|
+
reimplementation.
|