{
"package": "@colineapp/liquid-glass",
"version": "0.1.0",
"install": "npm install @colineapp/liquid-glass",
"summary": "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.",
"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."
],
"groups": [
{
"id": "options",
"title": "Glass options",
"intro": "Options for ``, `useLiquidGlass(ref, options)`, `createLiquidGlass(element, options)` and `glass.update(options)`. Every option is optional.",
"entries": [
{
"name": "appearance",
"type": "\"system\" | \"class\" | \"dark\" | \"light\"",
"default": "\"system\"",
"description": "`system` follows prefers-color-scheme. `class` is dark while `` has the `dark` class (Tailwind's class strategy, next-themes). `dark` and `light` fix it.",
"example": ""
},
{
"name": "variant",
"type": "\"regular\" | \"clear\" | \"lens\"",
"default": "\"regular\"",
"description": "`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`.",
"example": ""
},
{
"name": "floating",
"type": "boolean | \"dialog\"",
"default": "false",
"description": "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`).",
"example": ""
},
{
"name": "interactive",
"type": "boolean",
"default": "false",
"description": "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.",
"example": ""
},
{
"name": "swell",
"type": "boolean",
"default": "false",
"description": "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.",
"example": ""
},
{
"name": "tint",
"type": "string",
"default": "near-black in dark, white in light",
"description": "Colour washed over the body by the tone's `tint` share (50% on dark regular glass, 40% on light). Any CSS colour, including `var()`.",
"example": ""
},
{
"name": "hue",
"type": "string | null",
"default": "null",
"description": "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.",
"example": ""
},
{
"name": "hueStrength",
"type": "number (0–1)",
"default": "1 on light glass, 0.5 on dark",
"description": "How much of the hue the glass takes.",
"example": ""
},
{
"name": "geometry",
"type": "Partial",
"default": "DEFAULT_GEOMETRY",
"description": "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.",
"example": ""
},
{
"name": "tone",
"type": "{ dark?: Partial; light?: Partial }",
"description": "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`.",
"example": ""
},
{
"name": "fit",
"type": "\"box\" | \"fixed\" | \"scroll\"",
"default": "\"box\"",
"description": "`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.",
"example": ""
},
{
"name": "refraction",
"type": "boolean",
"default": "true",
"description": "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.",
"example": ""
},
{
"name": "samples",
"type": "1 | 4",
"default": "1",
"description": "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.",
"example": ""
}
]
},
{
"id": "geometry",
"title": "Geometry fields",
"intro": "Fields of `GlassGeometry`, in CSS pixels unless noted. Pass any subset as `geometry`.",
"entries": [
{
"name": "bezel",
"type": "number",
"default": "17",
"description": "Width of the refracting rim along the top and bottom. The centre is flat."
},
{
"name": "thickness",
"type": "number",
"default": "24",
"description": "Glass depth under the top and bottom rims. Deeper glass bends further."
},
{
"name": "sideBezel",
"type": "number",
"default": "27",
"description": "Width of the refracting rim at the sides and rounded ends."
},
{
"name": "sideThickness",
"type": "number",
"default": "44",
"description": "Glass depth under the sides and rounded ends."
},
{
"name": "refractiveIndex",
"type": "number",
"default": "1.5",
"description": "The glass's refractive index, for Snell's law at the rim."
},
{
"name": "maxFall",
"type": "number (0–1)",
"default": "0.8",
"description": "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."
},
{
"name": "edge",
"type": "number",
"default": "0.75",
"description": "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."
},
{
"name": "glint",
"type": "number (0–1)",
"default": "0.4",
"description": "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."
},
{
"name": "hairline",
"type": "number",
"default": "1",
"description": "Rim hairline width."
},
{
"name": "hairlineSides",
"type": "number (0–1)",
"default": "0.15",
"description": "Share of the hairline's strength that reaches the sides."
},
{
"name": "shadeWidth",
"type": "number",
"default": "1",
"description": "Width of the darkened sides."
},
{
"name": "glowReach",
"type": "number",
"default": "8",
"description": "How far the inner glow reaches in from the top and bottom rims."
},
{
"name": "glowBottom",
"type": "number (0–1)",
"default": "0.5",
"description": "Bottom glow strength relative to the top."
}
]
},
{
"id": "handle",
"title": "The glass handle",
"intro": "`createLiquidGlass()` and `attachLiquidGlass()` return it; `useLiquidGlass()` returns a ref to it, and `` fills one.",
"entries": [
{
"name": "update(options)",
"type": "(options: LiquidGlassOptions) => void",
"description": "Replaces the options. Cheap: the rim only rebuilds when the geometry changes.",
"example": "glass.update({ variant: \"clear\" });"
},
{
"name": "magnify(factor)",
"type": "(factor: number) => void",
"description": "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.",
"example": "glass.magnify(1.1);"
},
{
"name": "materialize(timing?)",
"type": "(timing?: { duration?, easing?, delay?, scale? }) => Promise",
"description": "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.",
"example": "await glass.materialize({ duration: 240, scale: 0.96 });"
},
{
"name": "dematerialize(timing?)",
"type": "(timing?) => Promise",
"description": "Fades the glass back to clear and holds it there, for surfaces that leave. Wells on it ease back with it.",
"example": "await glass.dematerialize({ duration: 160 });"
},
{
"name": "destroy()",
"type": "() => void",
"description": "Removes the glass and its filters."
},
{
"name": "element",
"type": "HTMLElement",
"description": "The glass element, the surface's first child."
},
{
"name": "appearance",
"type": "\"dark\" | \"light\"",
"description": "The resolved appearance."
}
]
},
{
"id": "tabs",
"title": "Tabs",
"intro": "`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() }`.",
"entries": [
{
"name": "appearance",
"type": "\"system\" | \"class\" | \"dark\" | \"light\"",
"default": "\"system\"",
"description": "As for glass."
},
{
"name": "orientation",
"type": "\"horizontal\" | \"vertical\"",
"default": "\"horizontal\"",
"description": "Vertical tabs stack and don't scrub."
},
{
"name": "tabs",
"type": "string",
"default": "'[role=\"tab\"]'",
"description": "Selector for the tabs inside the track.",
"example": "useLiquidGlassTabs(track, { tabs: '[role=\"radio\"]' });"
},
{
"name": "isSelected",
"type": "(tab: HTMLElement) => boolean",
"default": "aria-selected, aria-checked or data-state=\"active\"",
"description": "Whether a tab is selected. The thumb follows changes to the default attributes by itself; call `update()` after changes to anything else."
},
{
"name": "onSelect",
"type": "(tab: HTMLElement) => void",
"default": "press and click the tab",
"description": "Selects the tab the thumb was dragged onto."
},
{
"name": "glass",
"type": "boolean | LiquidGlassOptions",
"default": "glass unless on another glass surface",
"description": "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."
},
{
"name": "refraction",
"type": "boolean",
"default": "true",
"description": "Bend the page at the rims."
}
]
},
{
"id": "react",
"title": "React",
"intro": "From `@colineapp/liquid-glass/react`. React 18 or later; the core package has no dependencies.",
"entries": [
{
"name": "",
"type": "LiquidGlassOptions & { className?, style?, children?, glassRef? }",
"description": "Glass behind a surface's content. Render it first inside a positioned, rounded element. `glassRef` receives the handle, for materialize() and magnify().",
"example": ""
},
{
"name": "useLiquidGlass(ref, options)",
"type": "RefObject",
"description": "Turns an element you already render into a glass surface; the glass is inserted as its first child."
},
{
"name": "useLiquidGlassTabs(ref, options)",
"type": "RefObject",
"description": "Liquid Glass tabs on a track you render. See Tabs."
}
]
},
{
"id": "css",
"title": "CSS",
"intro": "Import `@colineapp/liquid-glass/styles.css` once. The filters themselves are set inline by the script, as literal values Safari can read.",
"entries": [
{
"name": ".liquid-glass-well",
"type": "class",
"description": "For a selected or hovered control on a glass surface: brightens the glass beneath it (dimmer in light), instead of glass on glass.",
"example": ""
},
{
"name": "--liquid-glass-shadow",
"type": "custom property",
"description": "The shadow floating glass casts. Set it on the surface or an ancestor."
},
{
"name": "--liquid-glass-dialog-shadow",
"type": "custom property",
"description": "The deeper shadow of `floating: \"dialog\"` glass."
}
]
}
],
"examples": [
{
"id": "react",
"title": "A glass toolbar in React",
"description": "Render first inside a positioned, rounded element.",
"language": "tsx",
"code": "import { LiquidGlass } from \"@colineapp/liquid-glass/react\";\nimport \"@colineapp/liquid-glass/styles.css\";\n\nexport function Toolbar() {\n return (\n \n );\n}"
},
{
"id": "vanilla",
"title": "Without React",
"description": "createLiquidGlass inserts the glass as the element's first child.",
"language": "ts",
"code": "import { createLiquidGlass } from \"@colineapp/liquid-glass\";\nimport \"@colineapp/liquid-glass/styles.css\";\n\nconst glass = createLiquidGlass(document.querySelector(\".toolbar\")!, {\n interactive: true,\n});\n\nglass.update({ variant: \"clear\" });\nglass.destroy();"
},
{
"id": "tabs",
"title": "Pill tabs",
"description": "Your tabs, your selection; the thumb follows aria-selected.",
"language": "tsx",
"code": "import { useRef } from \"react\";\nimport { useLiquidGlassTabs } from \"@colineapp/liquid-glass/react\";\n\nexport function Period({ value, onChange }: { value: string; onChange: (value: string) => void }) {\n const track = useRef(null);\n useLiquidGlassTabs(track, { appearance: \"class\" });\n return (\n
\n );\n}"
},
{
"id": "menu",
"title": "A menu that materializes",
"description": "Fade the contents, never the surface; the glass grows from clear.",
"language": "tsx",
"code": "import { useEffect, useRef, useState } from \"react\";\nimport { LiquidGlass, type LiquidGlassHandle } from \"@colineapp/liquid-glass/react\";\n\nexport function Menu({ open, items }: { open: boolean; items: string[] }) {\n const glass = useRef(null);\n const [active, setActive] = useState(-1);\n useEffect(() => {\n if (open) void glass.current?.materialize({ duration: 240, scale: 0.96 });\n }, [open]);\n if (!open) return null;\n return (\n
\n \n {items.map((item, index) => (\n \n ))}\n
\n );\n}"
},
{
"id": "dialog",
"title": "A glass dialog",
"description": "floating=\"dialog\" casts the deeper shadow; the glass materializes as the contents zoom in.",
"language": "tsx",
"code": "import { useLayoutEffect, useRef } from \"react\";\nimport { LiquidGlass, type LiquidGlassHandle } from \"@colineapp/liquid-glass/react\";\n\nexport function ShareDialog({ onClose }: { onClose: () => void }) {\n const glass = useRef(null);\n useLayoutEffect(() => {\n void glass.current?.materialize({ duration: 280, scale: 0.95 });\n }, []);\n const close = () => void glass.current?.dematerialize({ duration: 180, scale: 0.97 }).then(onClose);\n return (\n
\n \n
Share this album
\n \n
\n );\n}"
},
{
"id": "search",
"title": "A search field that swells",
"description": "interactive lights it under the pointer; swell springs it on press.",
"language": "tsx",
"code": ""
},
{
"id": "lens",
"title": "A magnifying lens",
"description": "The tab lens on its own: the lens variant, its geometry, four samples and magnify().",
"language": "tsx",
"code": "import { useRef } from \"react\";\nimport { LENS_GEOMETRY } from \"@colineapp/liquid-glass\";\nimport { LiquidGlass, type LiquidGlassHandle } from \"@colineapp/liquid-glass/react\";\n\nexport function Lens() {\n const lens = useRef(null);\n return (\n