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 |
| 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 surface, fade and zoom its contents, never the surface itself, and call materialize() and dematerialize() for the glass. |
| 43 | - Controls on glass get no glass of their own: give the selected or hovered one the liquid-glass-well class. |
| 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 | ### Pill tabs |
| 90 |
| 91 | Your tabs, your selection; the thumb follows aria-selected. |
| 92 |
| 93 | ```tsx |
| 94 | import { useRef } from "react"; |
| 95 | import { 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-1"> |
| 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 | {label} |
| 106 | </button> |
| 107 | ))} |
| 108 | </div> |
| 109 | ); |
| 110 | } |
| 111 | ``` |
| 112 |
| 113 | ### A menu that materializes |
| 114 |
| 115 | Fade the contents, never the surface; the glass grows from clear. |
| 116 |
| 117 | ```tsx |
| 118 | import { useEffect, useRef, useState } from "react"; |
| 119 | import { LiquidGlass, type LiquidGlassHandle } from "@colineapp/liquid-glass/react"; |
| 120 |
| 121 | export function Menu({ open, items }: { open: boolean; items: string[] }) { |
| 122 | const glass = useRef<LiquidGlassHandle | null>(null); |
| 123 | const [active, setActive] = useState(-1); |
| 124 | useEffect(() => { |
| 125 | if (open) void glass.current?.materialize({ duration: 240, scale: 0.96 }); |
| 126 | }, [open]); |
| 127 | if (!open) return null; |
| 128 | return ( |
| 129 | <div role="menu" className="absolute right-0 top-12 w-56 rounded-2xl p-1.5"> |
| 130 | <LiquidGlass glassRef={glass} appearance="class" floating /> |
| 131 | {items.map((item, index) => ( |
| 132 | <button key={item} role="menuitem" onPointerEnter={() => setActive(index)} |
| 133 | className={`w-full rounded-xl px-3 py-2 text-left ${index === active ? "liquid-glass-well" : ""}`}> |
| 134 | {item} |
| 135 | </button> |
| 136 | ))} |
| 137 | </div> |
| 138 | ); |
| 139 | } |
| 140 | ``` |
| 141 |
| 142 | ### A glass dialog |
| 143 |
| 144 | floating="dialog" casts the deeper shadow; the glass materializes as the contents zoom in. |
| 145 |
| 146 | ```tsx |
| 147 | import { useLayoutEffect, useRef } from "react"; |
| 148 | import { LiquidGlass, type LiquidGlassHandle } from "@colineapp/liquid-glass/react"; |
| 149 |
| 150 | export function ShareDialog({ onClose }: { onClose: () => void }) { |
| 151 | const glass = useRef<LiquidGlassHandle | null>(null); |
| 152 | useLayoutEffect(() => { |
| 153 | void glass.current?.materialize({ duration: 280, scale: 0.95 }); |
| 154 | }, []); |
| 155 | const close = () => void glass.current?.dematerialize({ duration: 180, scale: 0.97 }).then(onClose); |
| 156 | return ( |
| 157 | <div role="dialog" aria-modal="true" className="fixed left-1/2 top-1/3 w-80 -translate-x-1/2 rounded-[28px] p-5"> |
| 158 | <LiquidGlass glassRef={glass} appearance="class" floating="dialog" /> |
| 159 | <h2 className="font-semibold">Share this album</h2> |
| 160 | <button className="liquid-glass-well mt-4 h-10 w-full rounded-full" onClick={close}>Copy link</button> |
| 161 | </div> |
| 162 | ); |
| 163 | } |
| 164 | ``` |
| 165 |
| 166 | ### A search field that swells |
| 167 |
| 168 | interactive lights it under the pointer; swell springs it on press. |
| 169 |
| 170 | ```tsx |
| 171 | <label className="relative flex h-11 items-center gap-2 rounded-full px-4"> |
| 172 | <LiquidGlass appearance="class" interactive swell /> |
| 173 | <SearchIcon /> |
| 174 | <input className="bg-transparent outline-none" placeholder="Search" /> |
| 175 | </label> |
| 176 | ``` |
| 177 |
| 178 | ### A glass card |
| 179 |
| 180 | Regular glass on a rounded box: the rim bends what crosses it, and the body keeps the card's text legible. |
| 181 |
| 182 | ```tsx |
| 183 | import { LiquidGlass } from "@colineapp/liquid-glass/react"; |
| 184 | import "@colineapp/liquid-glass/styles.css"; |
| 185 |
| 186 | export function Card() { |
| 187 | return ( |
| 188 | <div className="relative grid gap-1 rounded-[30px] p-5"> |
| 189 | <LiquidGlass appearance="class" /> |
| 190 | <h3 className="font-semibold">Now playing</h3> |
| 191 | <p className="text-sm opacity-70">Glass takes the box's radius and follows its size.</p> |
| 192 | </div> |
| 193 | ); |
| 194 | } |
| 195 | ``` |
| 196 |