@aberhamm/liquid-glass-react 0.2.0 → 0.3.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/README.md +76 -618
- package/dist/index.cjs +87 -1
- package/dist/index.d.cts +103 -1150
- package/dist/index.d.ts +103 -1150
- package/dist/index.mjs +3280 -1224
- package/dist/liquid-glass-react.css +1 -1
- package/dist/native/index.native.d.ts +1 -1
- package/dist/native/index.native.js +1 -1
- package/licenses/samasante-liquid-glass-MIT.txt +21 -0
- package/package.json +8 -4
package/README.md
CHANGED
|
@@ -1,627 +1,85 @@
|
|
|
1
1
|
# @aberhamm/liquid-glass-react
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
React glass components powered by the pinned
|
|
4
|
+
[`@samasante/liquid-glass`](https://github.com/samasante/liquid-glass) engine.
|
|
5
|
+
This package owns the composed controls and their styling; the optics engine is
|
|
6
|
+
maintained upstream. Matui imports this package rather than maintaining a second
|
|
7
|
+
copy of the components.
|
|
7
8
|
|
|
8
|
-
|
|
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
|
|
9
|
+
```sh
|
|
38
10
|
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
11
|
```
|
|
61
12
|
|
|
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
13
|
```tsx
|
|
72
|
-
import {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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';
|
|
14
|
+
import {
|
|
15
|
+
Glass, GlassButton, GlassCard, GlassSwitch, GlassSlider,
|
|
16
|
+
GlassSegmentedControl, GlassNotification,
|
|
17
|
+
} from '@aberhamm/liquid-glass-react';
|
|
87
18
|
import '@aberhamm/liquid-glass-react/styles.css';
|
|
88
19
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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.
|
|
20
|
+
<Glass style={{ borderRadius: 28, padding: 24, background: '#ffffff33' }}
|
|
21
|
+
optics={{ frost: 3, strength: 0.2, dispersion: 0.6 }}>
|
|
22
|
+
A glass surface
|
|
23
|
+
</Glass>
|
|
24
|
+
<GlassButton>Save changes</GlassButton>
|
|
25
|
+
<GlassSwitch ariaLabel="Notifications" defaultChecked />
|
|
26
|
+
<GlassSlider ariaLabel="Volume" defaultValue={50} />
|
|
27
|
+
<GlassSegmentedControl label="Time range" defaultValue="week" options={[
|
|
28
|
+
{ value: 'day', label: 'Day' }, { value: 'week', label: 'Week' },
|
|
29
|
+
]} />
|
|
30
|
+
<div style={{ height: 400 }}>
|
|
31
|
+
<GlassNotification title="Matthew" avatar="MA" body="The glass collection is ready." />
|
|
32
|
+
</div>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
React 18+ and React DOM are peers. Import the stylesheet once for the composed
|
|
36
|
+
controls. `Glass` and its alias `LiquidGlass` expose the upstream engine's props.
|
|
37
|
+
Switch, slider and segmented control support controlled state or initial defaults,
|
|
38
|
+
native keyboard controls, form values and disabled states. Switch and slider motion
|
|
39
|
+
honors `prefers-reduced-motion`. Give all controls accessible labels.
|
|
40
|
+
|
|
41
|
+
The switch, slider and notification adapt the MIT upstream examples at revision
|
|
42
|
+
`4e7b769e1df7e5a7d3669fef22417fe3d2f79ade`; attribution is preserved in source
|
|
43
|
+
and `licenses/samasante-liquid-glass-MIT.txt`. The pinned dependency is bundled
|
|
44
|
+
in the web builds to support both ESM and CommonJS without requiring consumers
|
|
45
|
+
to load an ESM-only dependency through `require()`.
|
|
46
|
+
|
|
47
|
+
## Browser behavior
|
|
48
|
+
|
|
49
|
+
Arbitrary live-backdrop refraction is Chromium-only. Safari and Firefox retain
|
|
50
|
+
frosted glass; use `refract` with a decorative copy for cross-browser bending.
|
|
51
|
+
The notification recipe aligns a wallpaper copy beneath its lens. Keep copies
|
|
52
|
+
free of controls, IDs, data fetching and effects. The upstream video/canvas mode
|
|
53
|
+
uses WebGL. Large or numerous lenses can affect frame rate.
|
|
54
|
+
|
|
55
|
+
## Migrating from 0.2.x
|
|
56
|
+
|
|
57
|
+
Version 0.3 replaces the previous custom **web** implementation and its API.
|
|
58
|
+
Replace `cornerRadius` and `padding` with CSS in `style`; replace the old optical
|
|
59
|
+
props (`displacement`, `displacementScale`, `blurAmount`, etc.) with upstream
|
|
60
|
+
`optics`. `LiquidGlass` is now an alias for `Glass`, not a legacy-prop adapter.
|
|
61
|
+
The previous web capability, displacement, backdrop-sampling and motion helper
|
|
62
|
+
exports have been removed. Button/card/segmented-control support the props shown
|
|
63
|
+
in the new Storybook; consult the generated types for the complete new API.
|
|
64
|
+
Old docs and plans under `docs/` describe 0.2.x and remain historical references.
|
|
65
|
+
|
|
66
|
+
ESM, CommonJS and `styles.css` package paths remain available. The separate
|
|
67
|
+
`react-native` export condition still resolves to the previous native frosted
|
|
68
|
+
primitive backed by Expo Blur and Linear Gradient. Its existing props remain
|
|
69
|
+
unchanged; the new web-only composed controls are not exported on native.
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
corepack pnpm install
|
|
75
|
+
corepack pnpm lint
|
|
76
|
+
corepack pnpm typecheck
|
|
77
|
+
corepack pnpm test
|
|
78
|
+
corepack pnpm test:native
|
|
79
|
+
corepack pnpm build
|
|
80
|
+
corepack pnpm build-storybook
|
|
81
|
+
corepack pnpm exec playwright test
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Storybook contains examples for the engine and all six composed controls.
|
|
85
|
+
`test/consumer-fixture` checks ESM/CommonJS exports after a build.
|