npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@colineapp/liquid-glass

v0.3.0

Published

Liquid Glass for the web: physically based rim refraction, lighting and materials after iOS 27, on any element.

Readme

liquid-glass

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.

React

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>
  );
}

Any element

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.

The surface

Put the glass first inside a positioned, rounded element. It takes the element's corner radius, follows its size, and sits behind its content.

  • Paint nothing over it: no background, border or shadow on the surface.
  • No 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.
  • Animate a surface's position and size, not its scale. A changing scale leaves Chromium's backdrop blank for a few frames.

Options

| 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, as clear or tinted as its level; "clear" is regular glass at level 0, the page barely touched but for the rim; "lens" is the clear lens a control lifts into (pair with LENS_GEOMETRY) | | level | 0.5 | How clear or tinted regular glass is, as iOS 27's Liquid Glass slider sets it: 0 More Clear, 0.5 a light frost and a faint wash, 1 More Tinted (iOS 27's own default), anything in between. The rim doesn't change with it | | 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, a press blooms out from there while the glass swells, and light glass floats on a faint shadow (--liquid-glass-control-shadow) | | swell | as interactive | Pressed, the surface swells about its centre and springs back with a slight overshoot; held and dragged, it leans after the pointer like taffy. For glass you press, like a search field | | tone | | Per-appearance overrides of the variant's filters: { dark: { brightness: 0.8 }, light: { tint: 0.3 } } | | 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; rimShare, how much of the corner radius the top and bottom rims may take | | shape | | Glass of any outline: an SVG path (absolute M L H V C Z) in the surface's pixels, or a function from its size to one. Rendered whole per size, so keep it to surfaces that don't resize. Its rims stay within the surface's border-radius | | 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, "scroll" spans a surface that scrolls itself | | refraction | true | Bend the page at the rim where the browser supports it | | samples | 4 | Refraction samples per pixel. Four antialias the bend, so what it stretches doesn't stair-step; 1 is cheaper for large glass |

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 glass

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>

For the rows of a menu or list, attachLiquidGlassHighlight(surface) (or useLiquidGlassHighlight(ref)) adds a well that slides to the row under the pointer or with focus. Each edge rides its own spring, so it stretches toward the row it is heading for and gathers up as it lands. It finds menu items and options by default; pass items for other rows.

Menus that pour out of their button

On iOS 27 a menu is its button, grown: the button becomes a drop of glass that inflates into the menu over it, and closing, the menu shrinks back into the button as a drop, which bulges as it takes it back. pourFrom() and drawBack() do that with the surface's own glass, so it is the same glass on every frame, and carry the contents inside it.

const glass = useRef<LiquidGlassHandle | null>(null);
const button = useRef<HTMLButtonElement>(null);

// Once the menu is shown and placed:
useLayoutEffect(() => {
  if (open && button.current) void glass.current?.pourFrom(button.current, { cover: true });
}, [open]);

async function close() {
  // false if it was reopened while drawing back.
  if (await glass.current?.drawBack()) setShown(false);
}

<div role="menu" className="absolute rounded-3xl p-1.5">
  <LiquidGlass glassRef={glass} floating />
  …
</div>;
  • cover: true moves the menu over its button, as iOS 27's menus sit, for buttons up to 56px tall. Leave it off for a popover anchored to a field it mustn't hide; it still grows out of the field.
  • Pass a point ({ x, y }, client pixels) for a context menu, and submenu: true for a sub-menu, which unfolds out of its own edge beside its row and folds back faster.
  • side says which side of the source the surface sits on; it is measured when left out.
  • The menu's corners pass through every radius from 4 to 48px. Floating glass builds them ahead of time when it first appears; if your menus mount only as they open, call prewarmLiquidGlass() at startup so the first one pours out sharp.

Tabs

A horizontal track is iOS 27's segmented control, measured frame by frame from SwiftUI recordings. The selected tab is a solid thumb, white in light and grey in dark. Choosing a tab, it clears from the centre out into a clear lens larger than the track, over the labels, which bend at its rim; it stretches along its path with its speed, hangs at the tab it reached for a moment, and drops back in, refilling from the rim. 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. A vertical list is a sidebar instead: its selected tab is a well in the glass, and its lens grows less.

import { LiquidGlassTabLabel, 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-0.5">
      {["Day", "Week", "Month"].map((label) => (
        <button key={label} role="tab" aria-selected={label === value} onClick={() => onChange(label)}>
          <LiquidGlassTabLabel>{label}</LiquidGlassTabLabel>
        </button>
      ))}
    </div>
  );
}

Without React, attachLiquidGlassTabs(track, options) does the same and returns update() and destroy().

  • Your tabs and selection stay yours. The thumb follows aria-selected, aria-checked or Radix's data-state="active"; pass isSelected for anything else.
  • A tab the thumb is dragged onto gets pressed and clicked, as a pointer would. Pass onSelect to handle it yourself.
  • The selected label is semibold, and so is the one a drag would land on. LiquidGlassTabLabel holds each label at its semibold width, so tabs don't change width; without React, mark a label up as <span class="liquid-glass-tabs-label" data-text="Week"><span>Week</span></span>.
  • Give every label the same, primary text colour: the thumb alone marks the selection, as on iOS 27, and muted labels turn muddy over coloured glass.
  • On another glass surface the track gets no glass of its own: it becomes a recessed tint. glass: true or false decides it yourself.
  • --liquid-glass-tabs-tint sets the track's wash, and --liquid-glass-tabs-fill the thumb's colour.
  • orientation: "vertical" stacks the tabs. Vertical tabs don't scrub.

Appearing and leaving

materialize() grows the glass from clear to its tone, and dematerialize() fades it back and holds it there, so a dialog or sheet can open and close without fading its surface. Wells on it ease back to clear glass as it leaves. Both ease out on a cubic curve, 240ms in and 180ms out by default: on a steeper curve half of an opening lands in its first frame, and the glass seems to pop.

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({ scale: 0.965 });
}, [open]);

<LiquidGlass glassRef={glass} floating="dialog" />;

For CSS-driven animations, run the liquid-glass-materialize and liquid-glass-dematerialize keyframes on .liquid-glass-layer.

Accessibility

  • 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, the glass from swelling, and menus from pouring: a covered button still becomes its menu, at once.
  • forced-colors hides the glass and outlines the selected tab.

How it works

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. On small glass the top and bottom rims stop short of the middle (rimShare), so the body shows the page as it is. The result becomes an feDisplacementMap in a layer of its own over the body, masked to where the rim bends: the displacement takes the nearest pixel, and run over the whole body it drew the page coarser than the page itself on some GPUs. Four more masks draw 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. Glass of another outline is rendered whole: each pixel is shaded from its distance to the nearest point of the outline and the outline's normal there.

The tab thumb's centre and size ride one spring, as UIKit's do, and its stretch follows its speed a beat behind. A pouring menu's width, height and corner each ride their own: its height leads and overshoots, its width follows, and the corners stay round in flight and tighten as it lands. Every spring steps at a fixed 240 Hz, so motion is the same at any frame rate.

createGlassSlices() and createShapedGlass() expose the lens as raw pixels if you want to use it somewhere else.

Known gaps

This is a close approximation of Apple's glass, not a copy. Pull requests are welcome for any of these:

  • Separate glass doesn't merge. Apple's glass merges and splits as surfaces move near each other. Here glass swells when pressed and leans when dragged, the tab thumb stretches as it travels, and a menu pours out of its button as one drop, but two surfaces never flow into each other.
  • Light doesn't follow the device. The rim's glints are lit from a fixed top-left light. They don't move with the pointer or device tilt.
  • Refraction is Chromium only. Safari and Firefox don't run SVG filters in backdrop-filter, so they get the material and rim light without the bend.
  • Only the rim bends. The flat middle of the surface shows the page unmagnified, and there is no chromatic dispersion at the edge.
  • One radius per rounded box. The lens takes border-top-left-radius for every corner, so a box with mixed corner radii gets the wrong rim. Draw it as a shape instead, at the cost of a build per size.
  • Scaling the surface blanks it in Chromium for a few frames, so menus and dialogs can't use a scale-in animation on the surface itself (see Appearing and leaving).

Demo

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.

Source

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/main 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.

License

MIT