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.
| 1 | # Using @colineapp/liquid-glass |
| 2 | |
| 3 | For coding agents adding Liquid Glass to a web app. 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. |
| 4 | |
| 5 | Version 0.1.0. Every option has a plain-text page under https://liquid-glass.coline.space/docs/options/; the index is https://liquid-glass.coline.space/llms.txt. |
| 6 | |
| 7 | ## Install |
| 9 | ```sh |
| 10 | npm install @colineapp/liquid-glass |
| 11 | ``` |
| 12 |
| 13 | Import the stylesheet once, at the app's root: |
| 14 |
| 15 | ```ts |
| 16 | import "@colineapp/liquid-glass/styles.css"; |
| 17 | ``` |
| 18 |
| 19 | ## Pick the entry point |
| 20 |
| 21 | - React: `import { LiquidGlass, useLiquidGlass, useLiquidGlassTabs } from "@colineapp/liquid-glass/react"`. |
| 22 | - Anything else: `import { createLiquidGlass, attachLiquidGlassTabs } from "@colineapp/liquid-glass"`. |
| 23 | - With a class-based dark mode (Tailwind `dark:`, next-themes), pass `appearance="class"`. Otherwise the glass follows prefers-color-scheme. |
| 24 |
| 25 | ## Pick the options |
| 26 |
| 27 | | Surface | Options | |
| 28 | | --- | --- | |
| 29 | | Nav bar, toolbar, card | none, or `interactive` if it holds controls | |
| 30 | | Search field, button you press | `interactive swell` | |
| 31 | | Menu, popover | `floating`, and `materialize()` on open | |
| 32 | | Dialog, sheet | `floating="dialog"`, `fit="scroll"` if it scrolls itself | |
| 33 | | Glass over a photo or video | `variant="clear"` | |
| 34 | | Tinted glass | `hue="#3d6bff"` | |
| 35 | | Segmented control, tab bar | `useLiquidGlassTabs(trackRef)` on your own tabs | |
| 36 | | A magnifying lens | `variant="lens" geometry={LENS_GEOMETRY} samples={4}`, then `magnify()` | |
| 37 |
| 38 | ## Rules that keep the glass working |
| 39 |
| 40 | - Put the glass first inside a positioned (relative, absolute or fixed), rounded element. It takes the element's corner radius and follows its size. |
| 41 | - Paint nothing over it: no background, border or box-shadow on the surface. The glass draws its own edge. |
| 42 | - 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. |
| 43 | - To show or hide a surface, fade and zoom its contents, never the surface itself, and call materialize() and dematerialize() for the glass. |
| 44 | - Controls on glass get no glass of their own: give the selected or hovered one the liquid-glass-well class. |
| 45 | - Rim refraction renders in Chromium. Safari and Firefox get the same material, light and motion without the bend. |
| 46 |
| 47 | ## Checking your work |
| 48 |
| 49 | - The surface element has `data-liquid-glass="dark"` or `"light"` once the glass is attached, and its glass child has `data-ready` once the rim is built. |
| 50 | - In Chromium the glass child also has `data-refraction`. If the glass looks flat grey or empty, an ancestor has opacity, a filter, a mask or a backdrop-filter. |
| 51 | - Don't put a background colour on the surface to make it look glassy: the glass is the background. |
| 52 |
| 53 | ## Examples |
| 54 |
| 55 | ### A glass toolbar in React |
| 56 |
| 57 | Render <LiquidGlass /> first inside a positioned, rounded element. |
| 58 |
| 59 | ```tsx |
| 60 | import { LiquidGlass } from "@colineapp/liquid-glass/react"; |
| 61 | import "@colineapp/liquid-glass/styles.css"; |
| 62 |
| 63 | export function Toolbar() { |
| 64 | return ( |
| 65 | <nav className="relative flex gap-1 rounded-full p-1.5"> |
| 66 | <LiquidGlass appearance="class" interactive /> |
| 67 | <button className="liquid-glass-well rounded-full px-4 py-2">Today</button> |
| 68 | <button className="rounded-full px-4 py-2">Week</button> |
| 69 | </nav> |
| 70 | ); |
| 71 | } |
| 72 | ``` |
| 73 |
| 74 | ### Without React |
| 75 |
| 76 | createLiquidGlass inserts the glass as the element's first child. |
| 77 |
| 78 | ```ts |
| 79 | import { createLiquidGlass } from "@colineapp/liquid-glass"; |
| 80 | import "@colineapp/liquid-glass/styles.css"; |
| 81 |
| 82 | const glass = createLiquidGlass(document.querySelector(".toolbar")!, { |
| 83 | interactive: true, |
| 84 | }); |
| 85 |
| 86 | glass.update({ variant: "clear" }); |
| 87 | glass.destroy(); |
| 88 | ``` |
| 89 |
| 90 | ### Pill tabs |
| 91 |
| 92 | Your tabs, your selection; the thumb follows aria-selected. |
| 93 |
| 94 | ```tsx |
| 95 | import { useRef } from "react"; |
| 96 | import { useLiquidGlassTabs } from "@colineapp/liquid-glass/react"; |
| 97 |
| 98 | export function Period({ value, onChange }: { value: string; onChange: (value: string) => void }) { |
| 99 | const track = useRef<HTMLDivElement>(null); |
| 100 | useLiquidGlassTabs(track, { appearance: "class" }); |
| 101 | return ( |
| 102 | <div ref={track} role="tablist" className="flex rounded-full p-1"> |
| 103 | {["Day", "Week", "Month"].map((label) => ( |
| 104 | <button key={label} role="tab" aria-selected={label === value} onClick={() => onChange(label)} |
| 105 | className="rounded-full px-4 py-1.5 text-sm font-medium"> |
| 106 | {label} |
| 107 | </button> |
| 108 | ))} |
| 109 | </div> |
| 110 | ); |
| 111 | } |
| 112 | ``` |
| 113 |
| 114 | ### A menu that materializes |
| 115 |
| 116 | Fade the contents, never the surface; the glass grows from clear. |
| 117 |
| 118 | ```tsx |
| 119 | import { useEffect, useRef, useState } from "react"; |
| 120 | import { LiquidGlass, type LiquidGlassHandle } from "@colineapp/liquid-glass/react"; |
| 121 |
| 122 | export function Menu({ open, items }: { open: boolean; items: string[] }) { |
| 123 | const glass = useRef<LiquidGlassHandle | null>(null); |
| 124 | const [active, setActive] = useState(-1); |
| 125 | useEffect(() => { |
| 126 | if (open) void glass.current?.materialize({ duration: 240, scale: 0.96 }); |
| 127 | }, [open]); |
| 128 | if (!open) return null; |
| 129 | return ( |
| 130 | <div role="menu" className="absolute right-0 top-12 w-56 rounded-2xl p-1.5"> |
| 131 | <LiquidGlass glassRef={glass} appearance="class" floating /> |
| 132 | {items.map((item, index) => ( |
| 133 | <button key={item} role="menuitem" onPointerEnter={() => setActive(index)} |
| 134 | className={`w-full rounded-xl px-3 py-2 text-left ${index === active ? "liquid-glass-well" : ""}`}> |
| 135 | {item} |
| 136 | </button> |
| 137 | ))} |
| 138 | </div> |
| 139 | ); |
| 140 | } |
| 141 | ``` |
| 142 |
| 143 | ### A glass dialog |
| 144 |
| 145 | floating="dialog" casts the deeper shadow; the glass materializes as the contents zoom in. |
| 146 |
| 147 | ```tsx |
| 148 | import { useLayoutEffect, useRef } from "react"; |
| 149 | import { LiquidGlass, type LiquidGlassHandle } from "@colineapp/liquid-glass/react"; |
| 150 |
| 151 | export function ShareDialog({ onClose }: { onClose: () => void }) { |
| 152 | const glass = useRef<LiquidGlassHandle | null>(null); |
| 153 | useLayoutEffect(() => { |
| 154 | void glass.current?.materialize({ duration: 280, scale: 0.95 }); |
| 155 | }, []); |
| 156 | const close = () => void glass.current?.dematerialize({ duration: 180, scale: 0.97 }).then(onClose); |
| 157 | return ( |
| 158 | <div role="dialog" aria-modal="true" className="fixed left-1/2 top-1/3 w-80 -translate-x-1/2 rounded-[28px] p-5"> |
| 159 | <LiquidGlass glassRef={glass} appearance="class" floating="dialog" /> |
| 160 | <h2 className="font-semibold">Share this album</h2> |
| 161 | <button className="liquid-glass-well mt-4 h-10 w-full rounded-full" onClick={close}>Copy link</button> |
| 162 | </div> |
| 163 | ); |
| 164 | } |
| 165 | ``` |
| 166 |
| 167 | ### A search field that swells |
| 168 |
| 169 | interactive lights it under the pointer; swell springs it on press. |
| 170 |
| 171 | ```tsx |
| 172 | <label className="relative flex h-11 items-center gap-2 rounded-full px-4"> |
| 173 | <LiquidGlass appearance="class" interactive swell /> |
| 174 | <SearchIcon /> |
| 175 | <input className="bg-transparent outline-none" placeholder="Search" /> |
| 176 | </label> |
| 177 | ``` |
| 178 |
| 179 | ### A magnifying lens |
| 180 |
| 181 | The tab lens on its own: the lens variant, its geometry, four samples and magnify(). |
| 182 |
| 183 | ```tsx |
| 184 | import { useRef } from "react"; |
| 185 | import { LENS_GEOMETRY } from "@colineapp/liquid-glass"; |
| 186 | import { LiquidGlass, type LiquidGlassHandle } from "@colineapp/liquid-glass/react"; |
| 187 |
| 188 | export function Lens() { |
| 189 | const lens = useRef<LiquidGlassHandle | null>(null); |
| 190 | return ( |
| 191 | <div |
| 192 | className="absolute size-24 rounded-full" |
| 193 | onPointerDown={() => lens.current?.magnify(1.15)} |
| 194 | onPointerUp={() => lens.current?.magnify(1)} |
| 195 | > |
| 196 | <LiquidGlass glassRef={lens} appearance="class" variant="lens" geometry={LENS_GEOMETRY} samples={4} /> |
| 197 | </div> |
| 198 | ); |
| 199 | } |
| 200 | ``` |
| 201 |