# @colineapp/liquid-glass
> Liquid Glass for the web: the rim refraction, light and motion of iOS 27's glass on any rounded element, with React bindings and tabs whose thumb lifts into a lens. MIT.
Version 0.1.0. Install: `npm install @colineapp/liquid-glass`. npm: https://www.npmjs.com/package/@colineapp/liquid-glass. Playground: https://liquid-glass.coline.space.
## Quick start
```sh
npm install @colineapp/liquid-glass
```
```tsx
import { LiquidGlass } from "@colineapp/liquid-glass/react";
import "@colineapp/liquid-glass/styles.css";
```
## Rules
- Put the glass first inside a positioned (relative, absolute or fixed), rounded element. It takes the element's corner radius and follows its size.
- Paint nothing over it: no background, border or box-shadow on the surface. The glass draws its own edge.
- No opacity below 1, filter, mask or backdrop-filter on the surface or its ancestors while the glass shows. Each makes a backdrop root, and the glass would see only that element instead of the page.
- To show or hide a surface, fade and zoom its contents, never the surface itself, and call materialize() and dematerialize() for the glass.
- Controls on glass get no glass of their own: give the selected or hovered one the liquid-glass-well class.
- Rim refraction renders in Chromium. Safari and Firefox get the same material, light and motion without the bend.
## Glass options
Options for ``, `useLiquidGlass(ref, options)`, `createLiquidGlass(element, options)` and `glass.update(options)`. Every option is optional.
### appearance
- Type: `"system" | "class" | "dark" | "light"`
- Default: `"system"`
- Group: Glass options
`system` follows prefers-color-scheme. `class` is dark while `` has the `dark` class (Tailwind's class strategy, next-themes). `dark` and `light` fix it.
```tsx
```
### variant
- Type: `"regular" | "clear" | "lens"`
- Default: `"regular"`
- Group: Glass options
`regular` diffuses and evens out the page so text on the glass stays legible over anything. `clear` only bends and lights the page, for glass over media. `lens` is the clear lens a control lifts into; pair it with `geometry: LENS_GEOMETRY`.
```tsx
```
### floating
- Type: `boolean | "dialog"`
- Default: `false`
- Group: Glass options
For menus, popovers, dialogs and sheets. Floating glass diffuses the page more, as iOS 27's does, so text beneath washes into soft colour, and the surface casts a drop shadow (`--liquid-glass-shadow`). `"dialog"` casts a dialog's deeper shadow (`--liquid-glass-dialog-shadow`).
```tsx
```
### interactive
- Type: `boolean`
- Default: `false`
- Group: Glass options
For glass that holds controls, like SwiftUI's `.glassEffect(.regular.interactive())`: the pointer warms the glass beneath it, and a press blooms out from there.
```tsx
```
### swell
- Type: `boolean`
- Default: `false`
- Group: Glass options
For glass you press, like a search field: pressed, the surface swells about its centre on the tabs' spring and springs back with a slight overshoot on release.
```tsx
```
### tint
- Type: `string`
- Default: near-black in dark, white in light
- Group: Glass options
Colour washed over the body by the tone's `tint` share (50% on dark regular glass, 40% on light). Any CSS colour, including `var()`.
```tsx
```
### hue
- Type: `string | null`
- Default: `null`
- Group: Glass options
A colour whose hue the glass takes on, like tinted iOS glass: each pixel beneath shifts toward the hue at its own brightness, so colours behind keep their chroma. Any concrete CSS colour; neutral colours leave the glass untinted.
```tsx
```
### hueStrength
- Type: `number (0–1)`
- Default: 1 on light glass, 0.5 on dark
- Group: Glass options
How much of the hue the glass takes.
```tsx
```
### geometry
- Type: `Partial`
- Default: DEFAULT_GEOMETRY
- Group: Glass options
The rim's shape, measured from iOS 27: its width and depth, how steeply it bends, the darkened edge and the glints. See the geometry fields below. `LENS_GEOMETRY` is the rim for a control-high lens.
```tsx
```
### tone
- Type: `{ dark?: Partial; light?: Partial }`
- Group: Glass options
Per-appearance overrides of the variant's filters: body `blur`, `floatingBlur`, `saturate`, `contrast`, `brightness`, `tint` share, and the rim's `highlightContrast`, `highlightBrightness`, `shadeBrightness`, `glowSaturate`, `glowBrightness`, `edgeBrightness`.
```tsx
```
### fit
- Type: `"box" | "fixed" | "scroll"`
- Default: `"box"`
- Group: Glass options
`box` fills the surface. `fixed` fills the nearest containing block for fixed elements, such as a transformed popper wrapper, so the glass holds still while the surface scrolls its own content. `scroll` spans the surface's whole scroll height, for a dialog or sheet that scrolls itself.
```tsx
```
### refraction
- Type: `boolean`
- Default: `true`
- Group: Glass options
Bend the page at the rim. Rim refraction needs SVG filters in backdrop-filter, which only Chromium renders; Safari and Firefox keep the material without the bend either way.
```tsx
```
### samples
- Type: `1 | 4`
- Default: `1`
- Group: Glass options
Refraction samples per pixel. 4 antialiases the bend, so edges it stretches don't stair-step as the glass moves. It costs four displacement passes, so keep it for small glass with nothing blurring its backdrop, like a lens.
```tsx
```
## Geometry fields
Fields of `GlassGeometry`, in CSS pixels unless noted. Pass any subset as `geometry`.
### bezel
- Type: `number`
- Default: `17`
- Group: Geometry fields
Width of the refracting rim along the top and bottom. The centre is flat.
### thickness
- Type: `number`
- Default: `24`
- Group: Geometry fields
Glass depth under the top and bottom rims. Deeper glass bends further.
### sideBezel
- Type: `number`
- Default: `27`
- Group: Geometry fields
Width of the refracting rim at the sides and rounded ends.
### sideThickness
- Type: `number`
- Default: `44`
- Group: Geometry fields
Glass depth under the sides and rounded ends.
### refractiveIndex
- Type: `number`
- Default: `1.5`
- Group: Geometry fields
The glass's refractive index, for Snell's law at the rim.
### maxFall
- Type: `number (0–1)`
- Default: `0.8`
- Group: Geometry fields
Rim bend: the steepest the rim's shift may fall per pixel inward. Where it falls, the rim magnifies what is beneath by 1 / (1 − maxFall), so lower values bend more gently and keep text crossing the rim from smearing. The tab lens uses 0.3. Past 1 the lens folds content into mirrored echoes.
### edge
- Type: `number`
- Default: `0.75`
- Group: Geometry fields
Width of iOS 27's darkened edge, a contour where the glass meets the air, with the hairline just inside it. 0 leaves it out.
### glint
- Type: `number (0–1)`
- Default: `0.4`
- Group: Geometry fields
Share of the hairline's light that gathers into glints where the light catches the curve, at the top-left and, fainter, the bottom-right. 0 keeps an even band.
### hairline
- Type: `number`
- Default: `1`
- Group: Geometry fields
Rim hairline width.
### hairlineSides
- Type: `number (0–1)`
- Default: `0.15`
- Group: Geometry fields
Share of the hairline's strength that reaches the sides.
### shadeWidth
- Type: `number`
- Default: `1`
- Group: Geometry fields
Width of the darkened sides.
### glowReach
- Type: `number`
- Default: `8`
- Group: Geometry fields
How far the inner glow reaches in from the top and bottom rims.
### glowBottom
- Type: `number (0–1)`
- Default: `0.5`
- Group: Geometry fields
Bottom glow strength relative to the top.
## The glass handle
`createLiquidGlass()` and `attachLiquidGlass()` return it; `useLiquidGlass()` returns a ref to it, and `` fills one.
### update(options)
- Type: `(options: LiquidGlassOptions) => void`
- Group: The glass handle
Replaces the options. Cheap: the rim only rebuilds when the geometry changes.
```ts
glass.update({ variant: "clear" });
```
### magnify(factor)
- Type: `(factor: number) => void`
- Group: The glass handle
Magnifies what is beneath the glass about its centre, as a raised lens does (1 for none), and sharpens it so it doesn't come out soft. Cheap enough to call every frame. Needs refraction.
```ts
glass.magnify(1.1);
```
### materialize(timing?)
- Type: `(timing?: { duration?, easing?, delay?, scale? }) => Promise`
- Group: The glass handle
Grows the glass from clear to its tone, for surfaces that appear. Fade the surface's contents, never the surface: opacity below 1 hides the glass. For a surface that zooms open, scale its contents and pass the same `scale`; the glass grows by its inset instead.
```ts
await glass.materialize({ duration: 240, scale: 0.96 });
```
### dematerialize(timing?)
- Type: `(timing?) => Promise`
- Group: The glass handle
Fades the glass back to clear and holds it there, for surfaces that leave. Wells on it ease back with it.
```ts
await glass.dematerialize({ duration: 160 });
```
### destroy()
- Type: `() => void`
- Group: The glass handle
Removes the glass and its filters.
### element
- Type: `HTMLElement`
- Group: The glass handle
The glass element, the surface's first child.
### appearance
- Type: `"dark" | "light"`
- Group: The glass handle
The resolved appearance.
## Tabs
`useLiquidGlassTabs(trackRef, options)` in React, `attachLiquidGlassTabs(track, options)` without it. The track is the tabs' positioned, rounded container. Your tabs and selection stay yours; the thumb follows the selected tab, lifts into a lens that magnifies the labels, stretches as it travels and squashes as it lands. Dragging the selected tab scrubs across the others. Returns `{ update(), destroy() }`.
### appearance
- Type: `"system" | "class" | "dark" | "light"`
- Default: `"system"`
- Group: Tabs
As for glass.
### orientation
- Type: `"horizontal" | "vertical"`
- Default: `"horizontal"`
- Group: Tabs
Vertical tabs stack and don't scrub.
### tabs
- Type: `string`
- Default: '[role="tab"]'
- Group: Tabs
Selector for the tabs inside the track.
```ts
useLiquidGlassTabs(track, { tabs: '[role="radio"]' });
```
### isSelected
- Type: `(tab: HTMLElement) => boolean`
- Default: aria-selected, aria-checked or data-state="active"
- Group: Tabs
Whether a tab is selected. The thumb follows changes to the default attributes by itself; call `update()` after changes to anything else.
### onSelect
- Type: `(tab: HTMLElement) => void`
- Default: press and click the tab
- Group: Tabs
Selects the tab the thumb was dragged onto.
### glass
- Type: `boolean | LiquidGlassOptions`
- Default: glass unless on another glass surface
- Group: Tabs
The track's own glass, or its options. On another glass surface the track gets none of its own: it becomes a recessed tint and the thumb a well in that glass.
### refraction
- Type: `boolean`
- Default: `true`
- Group: Tabs
Bend the page at the rims.
## React
From `@colineapp/liquid-glass/react`. React 18 or later; the core package has no dependencies.
###
- Type: `LiquidGlassOptions & { className?, style?, children?, glassRef? }`
- Group: React
Glass behind a surface's content. Render it first inside a positioned, rounded element. `glassRef` receives the handle, for materialize() and magnify().
```tsx
```
### useLiquidGlass(ref, options)
- Type: `RefObject`
- Group: React
Turns an element you already render into a glass surface; the glass is inserted as its first child.
### useLiquidGlassTabs(ref, options)
- Type: `RefObject`
- Group: React
Liquid Glass tabs on a track you render. See Tabs.
## CSS
Import `@colineapp/liquid-glass/styles.css` once. The filters themselves are set inline by the script, as literal values Safari can read.
### .liquid-glass-well
- Type: `class`
- Group: CSS
For a selected or hovered control on a glass surface: brightens the glass beneath it (dimmer in light), instead of glass on glass.
```tsx
```
### --liquid-glass-shadow
- Type: `custom property`
- Group: CSS
The shadow floating glass casts. Set it on the surface or an ancestor.
### --liquid-glass-dialog-shadow
- Type: `custom property`
- Group: CSS
The deeper shadow of `floating: "dialog"` glass.