Liquid Glass for the web: the rim refraction, light and motion of iOS 27 glass on any rounded element. React bindings and pill tabs. MIT.
Liquid Glass for the web. Rim refraction, lighting and materials after iOS 27, computed from a lens profile instead of drawn by hand. One function for any element, a component for React, and tabs whose thumb lifts into a lens.
Playground and docs · Source · npm
npm install @colineapp/liquid-glass
| Browser | What you get |
|---|---|
| Chrome, Edge, Arc, Opera and other Chromium browsers | Full glass with rim refraction |
| Safari, Firefox | The same material and rim light, without the bend |
Refraction needs SVG filters inside backdrop-filter, which only Chromium
renders. Elsewhere the library leaves the filter out rather than lose the
backdrop.
import { LiquidGlass } from "@colineapp/liquid-glass/react";
import "@colineapp/liquid-glass/styles.css";
export function Toolbar() {
return (
<nav className="relative rounded-full px-6 py-3">
<LiquidGlass appearance="class" />
…
</nav>
);
}
import { createLiquidGlass } from "@colineapp/liquid-glass";
import "@colineapp/liquid-glass/styles.css";
const glass = createLiquidGlass(document.querySelector(".toolbar")!, { variant: "clear" });
glass.update({ variant: "regular" });
glass.destroy();
attachLiquidGlass(element, options) builds the glass inside an element you
render yourself, the way the React component does.
Put the glass first inside a positioned, rounded element. It takes the element's corner radius, follows its size, and sits behind its content.
opacity below 1, filter, mask or backdrop-filter on the surface or
its ancestors while the glass shows. Each of those makes a backdrop root, and
the glass would only see that element instead of the page.| Option | Default | |
|---|---|---|
appearance | "system" | "dark", "light", "system" (prefers-color-scheme) or "class" (dark while <html> has the dark class) |
variant | "regular" | "regular" diffuses the page so text stays legible; "clear" only bends and lights it; "lens" is the clear lens a control lifts into (pair with LENS_GEOMETRY) |
floating | false | For menus, popovers and dialogs: diffuses the page more, so text beneath doesn't read through, and the surface casts a shadow (--liquid-glass-shadow). "dialog" casts a dialog's deeper one (--liquid-glass-dialog-shadow) |
interactive | false | For glass that holds controls: the pointer warms the glass beneath it, and a press blooms out from there |
swell | false | For glass you press, like a search field: it swells about its centre and springs back on release, on the tabs' spring |
tone | Per-appearance overrides of the variant's filters: { dark: { brightness: 0.4 }, light: { tint: 0.5 } } | |
geometry | measured from iOS 27 | Rim width, depth and refractive index; the darkened edge and how much of the hairline gathers into glints; hairline, side shade and glow widths; maxFall, how steeply the rim may bend |
tint | near-black / white | Colour washed over the body by the tone's tint share. Any CSS colour, including var() |
hue | none | A colour whose hue the glass takes on. Neutral colours leave it untinted |
hueStrength | 1 light, 0.5 dark | How much of the hue it takes |
fit | "box" | "box" fills the surface, "fixed" fills a transformed popper wrapper, spans a surface that scrolls itself |
glass.magnify(factor) magnifies what is beneath the glass about its centre, as
a raised lens does (1 for none). It is cheap enough to call every frame, and
sharpens what it magnifies so it doesn't come out soft.
Controls on a glass surface shouldn't get glass of their own. Give the selected
or hovered one the liquid-glass-well class and it brightens the glass beneath
it.
<button className={selected ? "liquid-glass-well rounded-full" : "rounded-full"}>Today</button>
The track is a glass capsule and the selected tab a well in it. The motion is iOS 27's tab bar, measured from UIKit recordings. Choosing a tab, one thumb lifts off the track into a clear lens that magnifies the labels beneath it, stretches along its path with its speed, and squashes as it drops back in. The bar swells while pressed, and the press lights the glass under it. Pressing the selected tab lifts it in place, and dragging it scrubs across the others. A change of tab the viewer didn't make, such as a route change, slides flat.
import { useLiquidGlassTabs } from "@colineapp/liquid-glass/react";
export function Period({ value, onChange }) {
const track = useRef(null);
useLiquidGlassTabs(track);
return (
<div ref={track} role="tablist" className="flex rounded-full p-1">
{["Day", "Week", "Month"].map((label) => (
<button key={label} role="tab" aria-selected={label === value} onClick={() => onChange(label)}>
{label}
</button>
))}
</div>
);
}
Without React, attachLiquidGlassTabs(track, options) does the same and
returns update() and destroy().
aria-selected,
aria-checked or Radix's data-state="active"; pass isSelected for
anything else.onSelect to handle it yourself.glass: true or false decides it yourself.orientation: "vertical" stacks the tabs. Vertical tabs don't scrub.materialize() grows the glass from clear to its tone, and dematerialize()
fades it back and holds it there, so a menu can open and close without fading
its surface. Wells on it ease back to clear glass as it leaves.
For a surface that zooms as it opens, scale its contents and pass the same
scale: the glass grows by its inset instead, since scaling it blanks it.
const glass = useRef<LiquidGlassHandle | null>(null);
useLayoutEffect(() => {
if (open) void glass.current?.materialize({ duration: 240, scale: 0.96 });
}, [open]);
<LiquidGlass glassRef={glass} floating />;
For CSS-driven animations, run the liquid-glass-materialize and
liquid-glass-dematerialize keyframes on .liquid-glass-layer.
prefers-reduced-transparency replaces the glass with a solid fill of the tint colour.prefers-contrast: more darkens the sides and turns the edge into a contrasting border.prefers-reduced-motion keeps the tab thumb from lifting and travelling.forced-colors hides the glass and outlines the selected tab.The rim is a squircle-shaped bevel. For each pixel across it, a view ray meets
the tilted surface, refracts by Snell's law and travels through the glass to the
page. The shift is capped wherever it would fall faster than maxFall times the
rim moving inward. Past 1, neighbouring pixels would sample in reverse and fold
the page into mirrored echoes; lower caps bend more gently, which the tab lens
uses so labels crossing its rim don't smear. The result becomes an
feDisplacementMap in the surface's backdrop-filter, plus four masks: the rim
hairline, darkened sides, inner glow, and the darkened edge where the glass
meets the air. Part of the hairline gathers into glints where a light from the
top-left catches the curve, fainter where it returns at the bottom-right.
The lens is rendered once per corner radius as a rounded square of side 2r + 1 and cut into nine slices. Fixed corners and stretched edges assemble it over a box of any size, so surfaces can resize and change radius while the glass stays on.
The tab thumb's four edges each ride their own spring. Travelling, the leading edge runs on a stiffer one than the trailing edge, so the lens stretches along its path, then settles critically damped, since any overshoot would leave the glass wobbling by sub-pixels.
createGlassSlices() exposes the lens as raw pixels if you want to use it
somewhere else.
This is a close approximation of Apple's glass, not a copy. Pull requests are welcome for any of these:
backdrop-filter, so they get the material and rim light without the bend.border-top-left-radius for every
corner, so surfaces with mixed corner radii or non-rectangular shapes get the
wrong rim.The playground and docs are at https://liquid-glass.coline.space,
with plain-text docs for agents at /llms.txt and /agents.md. demo/ is
that site, a Coline Site built on the published package. Run it with
npm install && npm run dev inside that folder.
The repository is coline/liquid-glass on upstream.codes:
git clone https://git.coline.app/coline/liquid-glass.git
The package is developed inside Coline's monorepo, at packages/liquid-glass,
next to the app whose glass it ships, and this repository is published from
there. Issues and change requests are welcome here and get ported back. In the
monorepo, scripts/sync-liquid-glass.sh republishes it. It splits
packages/liquid-glass out of origin/0.7 with git subtree split, which
gives the same commits every time, and pushes the result here as main, so
every sync is a fast-forward.
MIT
"scroll"refraction | true | Bend the page at the rim where the browser supports it |
samples | 1 | Refraction samples per pixel. 4 antialiases the bend, so labels it stretches don't stair-step; it costs four passes, so keep it for small glass like a lens |