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, menus that pour out of their button, and segmented tabs whose thumb clears into a lens. MIT. |
| 4 | |
| 5 | Version 0.2.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 |
| 8 |
| 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 |
| 37 | ## Rules that keep the glass working |
| 38 |
| 39 | - Put the glass first inside a positioned (relative, absolute or fixed), rounded element. It takes the element's corner radius and follows its size. |
| 40 | - Paint nothing over it: no background, border or box-shadow on the surface. The glass draws its own edge. |
| 41 | - 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. |
| 42 | - To show or hide a dialog or sheet, fade and zoom its contents, never the surface itself, and call materialize() and dematerialize() for the glass. Menus and popovers pour out of their button with pourFrom() and draw back with drawBack(). |
| 43 | - Controls on glass get no glass of their own: give the selected or hovered one the liquid-glass-well class, or slide one between rows with attachLiquidGlassHighlight. |
| 44 | - Rim refraction renders in Chromium. Safari and Firefox get the same material, light and motion without the bend. |
| 45 |
| 46 | ## Checking your work |
| 47 |
| 48 | - 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. |
| 49 | - 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. |
| 50 | - Don't put a background colour on the surface to make it look glassy: the glass is the background. |
| 51 |
| 52 | ## Examples |
| 53 |
| 54 | ### A glass toolbar in React |
| 55 |
| 56 | Render <LiquidGlass /> first inside a positioned, rounded element. |
| 57 |
| 58 | ```tsx |
| 59 | import { LiquidGlass } from "@colineapp/liquid-glass/react"; |
| 60 | import "@colineapp/liquid-glass/styles.css"; |
| 61 |
| 62 | export function Toolbar() { |
| 63 | return ( |
| 64 | <nav className="relative flex gap-1 rounded-full p-1.5"> |
| 65 | <LiquidGlass appearance="class" interactive /> |
| 66 | <button className="liquid-glass-well rounded-full px-4 py-2">Today</button> |
| 67 | <button className="rounded-full px-4 py-2">Week</button> |
| 68 | </nav> |
| 69 | ); |
| 70 | } |
| 71 | ``` |
| 72 |
| 73 | ### Without React |
| 74 |
| 75 | createLiquidGlass inserts the glass as the element's first child. |
| 76 |
| 77 | ```ts |
| 78 | import { createLiquidGlass } from "@colineapp/liquid-glass"; |
| 79 | import "@colineapp/liquid-glass/styles.css"; |
| 80 |
| 81 | const glass = createLiquidGlass(document.querySelector(".toolbar")!, { |
| 82 | interactive: true, |
| 83 | }); |
| 84 |
| 85 | glass.update({ variant: "clear" }); |
| 86 | glass.destroy(); |
| 87 | ``` |
| 88 |
| 89 | ### Segmented tabs |
| 90 |
| 91 | Your tabs, your selection; the thumb follows aria-selected. Every label the same colour, semibold when selected. |
| 92 |
| 93 | ```tsx |
| 94 | import { useRef } from "react"; |
| 95 | import { LiquidGlassTabLabel, useLiquidGlassTabs } from "@colineapp/liquid-glass/react"; |
| 96 |
| 97 | export function Period({ value, onChange }: { value: string; onChange: (value: string) => void }) { |
| 98 | const track = useRef<HTMLDivElement>(null); |
| 99 | useLiquidGlassTabs(track, { appearance: "class" }); |
| 100 | return ( |
| 101 | <div ref={track} role="tablist" className="flex rounded-full p-0.5"> |
| 102 | {["Day", "Week", "Month"].map((label) => ( |
| 103 | <button key={label} role="tab" aria-selected={label === value} onClick={() => onChange(label)} |
| 104 | className="rounded-full px-4 py-1.5 text-sm font-medium"> |
| 105 | <LiquidGlassTabLabel>{label}</LiquidGlassTabLabel> |
| 106 | </button> |
| 107 | ))} |
| 108 | </div> |
| 109 | ); |
| 110 | } |
| 111 | ``` |
| 112 |
| 113 | ### A menu that pours out of its button |
| 114 |
| 115 | The button becomes the menu, and the menu draws back into it. A pill slides under the row you point at. |
| 116 |
| 117 | ```tsx |
| 118 | import { useLayoutEffect, useRef, type RefObject } from "react"; |
| 119 | import { LiquidGlass, useLiquidGlassHighlight, type LiquidGlassHandle } from "@colineapp/liquid-glass/react"; |
| 120 |
| 121 | // Render it once open, placed below its button. To close it: |
| 122 | // if (await glass.current?.drawBack()) setOpen(false); |
| 123 | export function Menu({ button, glass, items }: { |
| 124 | button: RefObject<HTMLButtonElement | null>; |
| 125 | glass: RefObject<LiquidGlassHandle | null>; |
| 126 | items: string[]; |
| 127 | }) { |
| 128 | const menu = useRef<HTMLDivElement>(null); |
| 129 | useLiquidGlassHighlight(menu); |
| 130 | useLayoutEffect(() => { |
| 131 | if (button.current) void glass.current?.pourFrom(button.current, { cover: true }); |
| 132 | }, [button, glass]); |
| 133 | return ( |
| 134 | <div ref={menu} role="menu" className="absolute left-0 top-12 w-56 rounded-3xl p-1.5"> |
| 135 | <LiquidGlass glassRef={glass} appearance="class" floating /> |
| 136 | {items.map((item) => ( |
| 137 | <button key={item} role="menuitem" className="w-full rounded-2xl px-3 py-2 text-left"> |
| 138 | {item} |
| 139 | </button> |
| 140 | ))} |
| 141 | </div> |
| 142 | ); |
| 143 | } |
| 144 | ``` |
| 145 |
| 146 | ### A glass dialog |
| 147 |
| 148 | floating="dialog" casts the deeper shadow; the glass materializes as the contents zoom in. |
| 149 |
| 150 | ```tsx |
| 151 | import { useLayoutEffect, useRef } from "react"; |
| 152 | import { LiquidGlass, type LiquidGlassHandle } from "@colineapp/liquid-glass/react"; |
| 153 |
| 154 | export function ShareDialog({ onClose }: { onClose: () => void }) { |
| 155 | const glass = useRef<LiquidGlassHandle | null>(null); |
| 156 | useLayoutEffect(() => { |
| 157 | void glass.current?.materialize({ scale: 0.965 }); |
| 158 | }, []); |
| 159 | const close = () => void glass.current?.dematerialize({ scale: 0.98 }).then(onClose); |
| 160 | return ( |
| 161 | <div role="dialog" aria-modal="true" className="fixed left-1/2 top-1/3 w-80 -translate-x-1/2 rounded-[28px] p-5"> |
| 162 | <LiquidGlass glassRef={glass} appearance="class" floating="dialog" /> |
| 163 | <h2 className="font-semibold">Share this album</h2> |
| 164 | <button className="liquid-glass-well mt-4 h-10 w-full rounded-full" onClick={close}>Copy link</button> |
| 165 | </div> |
| 166 | ); |
| 167 | } |
| 168 | ``` |
| 169 |
| 170 | ### A search field that swells |
| 171 |
| 172 | interactive lights it under the pointer, swells it on press and leans it after a drag. |
| 173 |
| 174 | ```tsx |
| 175 | <label className="relative flex h-11 items-center gap-2 rounded-full px-4"> |
| 176 | <LiquidGlass appearance="class" interactive /> |
| 177 | <SearchIcon /> |
| 178 | <input className="bg-transparent outline-none" placeholder="Search" /> |
| 179 | </label> |
| 180 | ``` |
| 181 |
| 182 | ### A glass card |
| 183 |
| 184 | Regular glass on a rounded box: the rim bends what crosses it, and the body keeps the card's text legible. |
| 185 |
| 186 | ```tsx |
| 187 | import { LiquidGlass } from "@colineapp/liquid-glass/react"; |
| 188 | import "@colineapp/liquid-glass/styles.css"; |
| 189 |
| 190 | export function Card() { |
| 191 | return ( |
| 192 | <div className="relative grid gap-1 rounded-[30px] p-5"> |
| 193 | <LiquidGlass appearance="class" /> |
| 194 | <h3 className="font-semibold">Now playing</h3> |
| 195 | <p className="text-sm opacity-70">Glass takes the box's radius and follows its size.</p> |
| 196 | </div> |
| 197 | ); |
| 198 | } |
| 199 | ``` |
| 200 |