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 | # @colineapp/liquid-glass |
| 2 | |
| 3 | > 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. Install: `npm install @colineapp/liquid-glass`. npm: https://www.npmjs.com/package/@colineapp/liquid-glass. Playground: https://liquid-glass.coline.space. |
| 6 | |
| 7 | ## Quick start |
| 8 | |
| 9 |
| ```sh |
| 10 | npm install @colineapp/liquid-glass |
| 11 | ``` |
| 12 |
| 13 | ```tsx |
| 14 | import { LiquidGlass } from "@colineapp/liquid-glass/react"; |
| 15 | import "@colineapp/liquid-glass/styles.css"; |
| 16 |
| 17 | <nav className="relative rounded-full px-6 py-3"> |
| 18 | <LiquidGlass appearance="class" /> |
| 19 | … |
| 20 | </nav> |
| 21 | ``` |
| 22 |
| 23 | ## Rules |
| 24 |
| 25 | - Put the glass first inside a positioned (relative, absolute or fixed), rounded element. It takes the element's corner radius and follows its size. |
| 26 | - Paint nothing over it: no background, border or box-shadow on the surface. The glass draws its own edge. |
| 27 | - 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. |
| 28 | - To show or hide a surface, fade and zoom its contents, never the surface itself, and call materialize() and dematerialize() for the glass. |
| 29 | - Controls on glass get no glass of their own: give the selected or hovered one the liquid-glass-well class. |
| 30 | - Rim refraction renders in Chromium. Safari and Firefox get the same material, light and motion without the bend. |
| 31 |
| 32 | ## Glass options |
| 33 |
| 34 | Options for `<LiquidGlass />`, `useLiquidGlass(ref, options)`, `createLiquidGlass(element, options)` and `glass.update(options)`. Every option is optional. |
| 35 |
| 36 | ### appearance |
| 37 |
| 38 | - Type: `"system" | "class" | "dark" | "light"` |
| 39 | - Default: `"system"` |
| 40 | - Group: Glass options |
| 41 |
| 42 | `system` follows prefers-color-scheme. `class` is dark while `<html>` has the `dark` class (Tailwind's class strategy, next-themes). `dark` and `light` fix it. |
| 43 |
| 44 | ```tsx |
| 45 | <LiquidGlass appearance="class" /> |
| 46 | ``` |
| 47 |
| 48 | ### variant |
| 49 |
| 50 | - Type: `"regular" | "clear" | "lens"` |
| 51 | - Default: `"regular"` |
| 52 | - Group: Glass options |
| 53 |
| 54 | `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`. |
| 55 |
| 56 | ```tsx |
| 57 | <LiquidGlass variant="clear" /> |
| 58 | ``` |
| 59 |
| 60 | ### floating |
| 61 |
| 62 | - Type: `boolean | "dialog"` |
| 63 | - Default: `false` |
| 64 | - Group: Glass options |
| 65 |
| 66 | 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`). |
| 67 |
| 68 | ```tsx |
| 69 | <LiquidGlass floating="dialog" /> |
| 70 | ``` |
| 71 |
| 72 | ### interactive |
| 73 |
| 74 | - Type: `boolean` |
| 75 | - Default: `false` |
| 76 | - Group: Glass options |
| 77 |
| 78 | 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. |
| 79 |
| 80 | ```tsx |
| 81 | <LiquidGlass interactive /> |
| 82 | ``` |
| 83 |
| 84 | ### swell |
| 85 |
| 86 | - Type: `boolean` |
| 87 | - Default: `false` |
| 88 | - Group: Glass options |
| 89 |
| 90 | 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. |
| 91 |
| 92 | ```tsx |
| 93 | <LiquidGlass interactive swell /> |
| 94 | ``` |
| 95 |
| 96 | ### tint |
| 97 |
| 98 | - Type: `string` |
| 99 | - Default: near-black in dark, white in light |
| 100 | - Group: Glass options |
| 101 |
| 102 | Colour washed over the body by the tone's `tint` share (50% on dark regular glass, 40% on light). Any CSS colour, including `var()`. |
| 103 |
| 104 | ```tsx |
| 105 | <LiquidGlass tint="var(--background)" /> |
| 106 | ``` |
| 107 |
| 108 | ### hue |
| 109 |
| 110 | - Type: `string | null` |
| 111 | - Default: `null` |
| 112 | - Group: Glass options |
| 113 |
| 114 | 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. |
| 115 |
| 116 | ```tsx |
| 117 | <LiquidGlass hue="#3d6bff" /> |
| 118 | ``` |
| 119 |
| 120 | ### hueStrength |
| 121 |
| 122 | - Type: `number (0–1)` |
| 123 | - Default: 1 on light glass, 0.5 on dark |
| 124 | - Group: Glass options |
| 125 |
| 126 | How much of the hue the glass takes. |
| 127 |
| 128 | ```tsx |
| 129 | <LiquidGlass hue="#ff4fa3" hueStrength={0.7} /> |
| 130 | ``` |
| 131 |
| 132 | ### geometry |
| 133 |
| 134 | - Type: `Partial<GlassGeometry>` |
| 135 | - Default: DEFAULT_GEOMETRY |
| 136 | - Group: Glass options |
| 137 |
| 138 | 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. |
| 139 |
| 140 | ```tsx |
| 141 | <LiquidGlass geometry={{ maxFall: 0.5, edge: 1 }} /> |
| 142 | ``` |
| 143 |
| 144 | ### tone |
| 145 |
| 146 | - Type: `{ dark?: Partial<GlassTone>; light?: Partial<GlassTone> }` |
| 147 | - Group: Glass options |
| 148 |
| 149 | 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`. |
| 150 |
| 151 | ```tsx |
| 152 | <LiquidGlass tone={{ dark: { brightness: 0.4 }, light: { tint: 0.5 } }} /> |
| 153 | ``` |
| 154 |
| 155 | ### fit |
| 156 |
| 157 | - Type: `"box" | "fixed" | "scroll"` |
| 158 | - Default: `"box"` |
| 159 | - Group: Glass options |
| 160 |
| 161 | `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. |
| 162 |
| 163 | ```tsx |
| 164 | <LiquidGlass fit="scroll" floating="dialog" /> |
| 165 | ``` |
| 166 |
| 167 | ### refraction |
| 168 |
| 169 | - Type: `boolean` |
| 170 | - Default: `true` |
| 171 | - Group: Glass options |
| 172 |
| 173 | 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. |
| 174 |
| 175 | ```tsx |
| 176 | <LiquidGlass refraction={false} /> |
| 177 | ``` |
| 178 |
| 179 | ### samples |
| 180 |
| 181 | - Type: `1 | 4` |
| 182 | - Default: `1` |
| 183 | - Group: Glass options |
| 184 |
| 185 | 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. |
| 186 |
| 187 | ```tsx |
| 188 | <LiquidGlass variant="lens" samples={4} /> |
| 189 | ``` |
| 190 |
| 191 | ## Geometry fields |
| 192 |
| 193 | Fields of `GlassGeometry`, in CSS pixels unless noted. Pass any subset as `geometry`. |
| 194 |
| 195 | ### bezel |
| 196 |
| 197 | - Type: `number` |
| 198 | - Default: `17` |
| 199 | - Group: Geometry fields |
| 200 |
| 201 | Width of the refracting rim along the top and bottom. The centre is flat. |
| 202 |
| 203 | ### thickness |
| 204 |
| 205 | - Type: `number` |
| 206 | - Default: `24` |
| 207 | - Group: Geometry fields |
| 208 |
| 209 | Glass depth under the top and bottom rims. Deeper glass bends further. |
| 210 |
| 211 | ### sideBezel |
| 212 |
| 213 | - Type: `number` |
| 214 | - Default: `27` |
| 215 | - Group: Geometry fields |
| 216 |
| 217 | Width of the refracting rim at the sides and rounded ends. |
| 218 |
| 219 | ### sideThickness |
| 220 |
| 221 | - Type: `number` |
| 222 | - Default: `44` |
| 223 | - Group: Geometry fields |
| 224 |
| 225 | Glass depth under the sides and rounded ends. |
| 226 |
| 227 | ### refractiveIndex |
| 228 |
| 229 | - Type: `number` |
| 230 | - Default: `1.5` |
| 231 | - Group: Geometry fields |
| 232 |
| 233 | The glass's refractive index, for Snell's law at the rim. |
| 234 |
| 235 | ### maxFall |
| 236 |
| 237 | - Type: `number (0–1)` |
| 238 | - Default: `0.8` |
| 239 | - Group: Geometry fields |
| 240 |
| 241 | 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. |
| 242 |
| 243 | ### edge |
| 244 |
| 245 | - Type: `number` |
| 246 | - Default: `0.75` |
| 247 | - Group: Geometry fields |
| 248 |
| 249 | 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. |
| 250 |
| 251 | ### glint |
| 252 |
| 253 | - Type: `number (0–1)` |
| 254 | - Default: `0.4` |
| 255 | - Group: Geometry fields |
| 256 |
| 257 | 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. |
| 258 |
| 259 | ### hairline |
| 260 |
| 261 | - Type: `number` |
| 262 | - Default: `1` |
| 263 | - Group: Geometry fields |
| 264 |
| 265 | Rim hairline width. |
| 266 |
| 267 | ### hairlineSides |
| 268 |
| 269 | - Type: `number (0–1)` |
| 270 | - Default: `0.15` |
| 271 | - Group: Geometry fields |
| 272 |
| 273 | Share of the hairline's strength that reaches the sides. |
| 274 |
| 275 | ### shadeWidth |
| 276 |
| 277 | - Type: `number` |
| 278 | - Default: `1` |
| 279 | - Group: Geometry fields |
| 280 |
| 281 | Width of the darkened sides. |
| 282 |
| 283 | ### glowReach |
| 284 |
| 285 | - Type: `number` |
| 286 | - Default: `8` |
| 287 | - Group: Geometry fields |
| 288 |
| 289 | How far the inner glow reaches in from the top and bottom rims. |
| 290 |
| 291 | ### glowBottom |
| 292 |
| 293 | - Type: `number (0–1)` |
| 294 | - Default: `0.5` |
| 295 | - Group: Geometry fields |
| 296 |
| 297 | Bottom glow strength relative to the top. |
| 298 |
| 299 | ## The glass handle |
| 300 |
| 301 | `createLiquidGlass()` and `attachLiquidGlass()` return it; `useLiquidGlass()` returns a ref to it, and `<LiquidGlass glassRef={ref} />` fills one. |
| 302 |
| 303 | ### update(options) |
| 304 |
| 305 | - Type: `(options: LiquidGlassOptions) => void` |
| 306 | - Group: The glass handle |
| 307 |
| 308 | Replaces the options. Cheap: the rim only rebuilds when the geometry changes. |
| 309 |
| 310 | ```ts |
| 311 | glass.update({ variant: "clear" }); |
| 312 | ``` |
| 313 |
| 314 | ### magnify(factor) |
| 315 |
| 316 | - Type: `(factor: number) => void` |
| 317 | - Group: The glass handle |
| 318 |
| 319 | 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. |
| 320 |
| 321 | ```ts |
| 322 | glass.magnify(1.1); |
| 323 | ``` |
| 324 |
| 325 | ### materialize(timing?) |
| 326 |
| 327 | - Type: `(timing?: { duration?, easing?, delay?, scale? }) => Promise<void>` |
| 328 | - Group: The glass handle |
| 329 |
| 330 | 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. |
| 331 |
| 332 | ```ts |
| 333 | await glass.materialize({ duration: 240, scale: 0.96 }); |
| 334 | ``` |
| 335 |
| 336 | ### dematerialize(timing?) |
| 337 |
| 338 | - Type: `(timing?) => Promise<void>` |
| 339 | - Group: The glass handle |
| 340 |
| 341 | Fades the glass back to clear and holds it there, for surfaces that leave. Wells on it ease back with it. |
| 342 |
| 343 | ```ts |
| 344 | await glass.dematerialize({ duration: 160 }); |
| 345 | ``` |
| 346 |
| 347 | ### destroy() |
| 348 |
| 349 | - Type: `() => void` |
| 350 | - Group: The glass handle |
| 351 |
| 352 | Removes the glass and its filters. |
| 353 |
| 354 | ### element |
| 355 |
| 356 | - Type: `HTMLElement` |
| 357 | - Group: The glass handle |
| 358 |
| 359 | The glass element, the surface's first child. |
| 360 |
| 361 | ### appearance |
| 362 |
| 363 | - Type: `"dark" | "light"` |
| 364 | - Group: The glass handle |
| 365 |
| 366 | The resolved appearance. |
| 367 |
| 368 | ## Tabs |
| 369 |
| 370 | `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() }`. |
| 371 |
| 372 | ### appearance |
| 373 |
| 374 | - Type: `"system" | "class" | "dark" | "light"` |
| 375 | - Default: `"system"` |
| 376 | - Group: Tabs |
| 377 |
| 378 | As for glass. |
| 379 |
| 380 | ### orientation |
| 381 |
| 382 | - Type: `"horizontal" | "vertical"` |
| 383 | - Default: `"horizontal"` |
| 384 | - Group: Tabs |
| 385 |
| 386 | Vertical tabs stack and don't scrub. |
| 387 |
| 388 | ### tabs |
| 389 |
| 390 | - Type: `string` |
| 391 | - Default: '[role="tab"]' |
| 392 | - Group: Tabs |
| 393 |
| 394 | Selector for the tabs inside the track. |
| 395 |
| 396 | ```ts |
| 397 | useLiquidGlassTabs(track, { tabs: '[role="radio"]' }); |
| 398 | ``` |
| 399 |
| 400 | ### isSelected |
| 401 |
| 402 | - Type: `(tab: HTMLElement) => boolean` |
| 403 | - Default: aria-selected, aria-checked or data-state="active" |
| 404 | - Group: Tabs |
| 405 |
| 406 | Whether a tab is selected. The thumb follows changes to the default attributes by itself; call `update()` after changes to anything else. |
| 407 |
| 408 | ### onSelect |
| 409 |
| 410 | - Type: `(tab: HTMLElement) => void` |
| 411 | - Default: press and click the tab |
| 412 | - Group: Tabs |
| 413 |
| 414 | Selects the tab the thumb was dragged onto. |
| 415 |
| 416 | ### glass |
| 417 |
| 418 | - Type: `boolean | LiquidGlassOptions` |
| 419 | - Default: glass unless on another glass surface |
| 420 | - Group: Tabs |
| 421 |
| 422 | 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. |
| 423 |
| 424 | ### refraction |
| 425 |
| 426 | - Type: `boolean` |
| 427 | - Default: `true` |
| 428 | - Group: Tabs |
| 429 |
| 430 | Bend the page at the rims. |
| 431 |
| 432 | ## React |
| 433 |
| 434 | From `@colineapp/liquid-glass/react`. React 18 or later; the core package has no dependencies. |
| 435 |
| 436 | ### <LiquidGlass /> |
| 437 |
| 438 | - Type: `LiquidGlassOptions & { className?, style?, children?, glassRef? }` |
| 439 | - Group: React |
| 440 |
| 441 | Glass behind a surface's content. Render it first inside a positioned, rounded element. `glassRef` receives the handle, for materialize() and magnify(). |
| 442 |
| 443 | ```tsx |
| 444 | <nav className="relative rounded-full px-6 py-3"> |
| 445 | <LiquidGlass interactive /> |
| 446 | … |
| 447 | </nav> |
| 448 | ``` |
| 449 |
| 450 | ### useLiquidGlass(ref, options) |
| 451 |
| 452 | - Type: `RefObject<LiquidGlass | null>` |
| 453 | - Group: React |
| 454 |
| 455 | Turns an element you already render into a glass surface; the glass is inserted as its first child. |
| 456 |
| 457 | ### useLiquidGlassTabs(ref, options) |
| 458 |
| 459 | - Type: `RefObject<LiquidGlassTabs | null>` |
| 460 | - Group: React |
| 461 |
| 462 | Liquid Glass tabs on a track you render. See Tabs. |
| 463 |
| 464 | ## CSS |
| 465 |
| 466 | Import `@colineapp/liquid-glass/styles.css` once. The filters themselves are set inline by the script, as literal values Safari can read. |
| 467 |
| 468 | ### .liquid-glass-well |
| 469 |
| 470 | - Type: `class` |
| 471 | - Group: CSS |
| 472 |
| 473 | For a selected or hovered control on a glass surface: brightens the glass beneath it (dimmer in light), instead of glass on glass. |
| 474 |
| 475 | ```tsx |
| 476 | <button className="liquid-glass-well rounded-full">Today</button> |
| 477 | ``` |
| 478 |
| 479 | ### --liquid-glass-shadow |
| 480 |
| 481 | - Type: `custom property` |
| 482 | - Group: CSS |
| 483 |
| 484 | The shadow floating glass casts. Set it on the surface or an ancestor. |
| 485 |
| 486 | ### --liquid-glass-dialog-shadow |
| 487 |
| 488 | - Type: `custom property` |
| 489 | - Group: CSS |
| 490 |
| 491 | The deeper shadow of `floating: "dialog"` glass. |
| 492 |
| 493 | # Examples |
| 494 |
| 495 | ## A glass toolbar in React |
| 496 |
| 497 | Render <LiquidGlass /> first inside a positioned, rounded element. |
| 498 |
| 499 | ```tsx |
| 500 | import { LiquidGlass } from "@colineapp/liquid-glass/react"; |
| 501 | import "@colineapp/liquid-glass/styles.css"; |
| 502 |
| 503 | export function Toolbar() { |
| 504 | return ( |
| 505 | <nav className="relative flex gap-1 rounded-full p-1.5"> |
| 506 | <LiquidGlass appearance="class" interactive /> |
| 507 | <button className="liquid-glass-well rounded-full px-4 py-2">Today</button> |
| 508 | <button className="rounded-full px-4 py-2">Week</button> |
| 509 | </nav> |
| 510 | ); |
| 511 | } |
| 512 | ``` |
| 513 |
| 514 | ## Without React |
| 515 |
| 516 | createLiquidGlass inserts the glass as the element's first child. |
| 517 |
| 518 | ```ts |
| 519 | import { createLiquidGlass } from "@colineapp/liquid-glass"; |
| 520 | import "@colineapp/liquid-glass/styles.css"; |
| 521 |
| 522 | const glass = createLiquidGlass(document.querySelector(".toolbar")!, { |
| 523 | interactive: true, |
| 524 | }); |
| 525 |
| 526 | glass.update({ variant: "clear" }); |
| 527 | glass.destroy(); |
| 528 | ``` |
| 529 |
| 530 | ## Pill tabs |
| 531 |
| 532 | Your tabs, your selection; the thumb follows aria-selected. |
| 533 |
| 534 | ```tsx |
| 535 | import { useRef } from "react"; |
| 536 | import { useLiquidGlassTabs } from "@colineapp/liquid-glass/react"; |
| 537 |
| 538 | export function Period({ value, onChange }: { value: string; onChange: (value: string) => void }) { |
| 539 | const track = useRef<HTMLDivElement>(null); |
| 540 | useLiquidGlassTabs(track, { appearance: "class" }); |
| 541 | return ( |
| 542 | <div ref={track} role="tablist" className="flex rounded-full p-1"> |
| 543 | {["Day", "Week", "Month"].map((label) => ( |
| 544 | <button key={label} role="tab" aria-selected={label === value} onClick={() => onChange(label)} |
| 545 | className="rounded-full px-4 py-1.5 text-sm font-medium"> |
| 546 | {label} |
| 547 | </button> |
| 548 | ))} |
| 549 | </div> |
| 550 | ); |
| 551 | } |
| 552 | ``` |
| 553 |
| 554 | ## A menu that materializes |
| 555 |
| 556 | Fade the contents, never the surface; the glass grows from clear. |
| 557 |
| 558 | ```tsx |
| 559 | import { useEffect, useRef, useState } from "react"; |
| 560 | import { LiquidGlass, type LiquidGlassHandle } from "@colineapp/liquid-glass/react"; |
| 561 |
| 562 | export function Menu({ open, items }: { open: boolean; items: string[] }) { |
| 563 | const glass = useRef<LiquidGlassHandle | null>(null); |
| 564 | const [active, setActive] = useState(-1); |
| 565 | useEffect(() => { |
| 566 | if (open) void glass.current?.materialize({ duration: 240, scale: 0.96 }); |
| 567 | }, [open]); |
| 568 | if (!open) return null; |
| 569 | return ( |
| 570 | <div role="menu" className="absolute right-0 top-12 w-56 rounded-2xl p-1.5"> |
| 571 | <LiquidGlass glassRef={glass} appearance="class" floating /> |
| 572 | {items.map((item, index) => ( |
| 573 | <button key={item} role="menuitem" onPointerEnter={() => setActive(index)} |
| 574 | className={`w-full rounded-xl px-3 py-2 text-left ${index === active ? "liquid-glass-well" : ""}`}> |
| 575 | {item} |
| 576 | </button> |
| 577 | ))} |
| 578 | </div> |
| 579 | ); |
| 580 | } |
| 581 | ``` |
| 582 |
| 583 | ## A glass dialog |
| 584 |
| 585 | floating="dialog" casts the deeper shadow; the glass materializes as the contents zoom in. |
| 586 |
| 587 | ```tsx |
| 588 | import { useLayoutEffect, useRef } from "react"; |
| 589 | import { LiquidGlass, type LiquidGlassHandle } from "@colineapp/liquid-glass/react"; |
| 590 |
| 591 | export function ShareDialog({ onClose }: { onClose: () => void }) { |
| 592 | const glass = useRef<LiquidGlassHandle | null>(null); |
| 593 | useLayoutEffect(() => { |
| 594 | void glass.current?.materialize({ duration: 280, scale: 0.95 }); |
| 595 | }, []); |
| 596 | const close = () => void glass.current?.dematerialize({ duration: 180, scale: 0.97 }).then(onClose); |
| 597 | return ( |
| 598 | <div role="dialog" aria-modal="true" className="fixed left-1/2 top-1/3 w-80 -translate-x-1/2 rounded-[28px] p-5"> |
| 599 | <LiquidGlass glassRef={glass} appearance="class" floating="dialog" /> |
| 600 | <h2 className="font-semibold">Share this album</h2> |
| 601 | <button className="liquid-glass-well mt-4 h-10 w-full rounded-full" onClick={close}>Copy link</button> |
| 602 | </div> |
| 603 | ); |
| 604 | } |
| 605 | ``` |
| 606 |
| 607 | ## A search field that swells |
| 608 |
| 609 | interactive lights it under the pointer; swell springs it on press. |
| 610 |
| 611 | ```tsx |
| 612 | <label className="relative flex h-11 items-center gap-2 rounded-full px-4"> |
| 613 | <LiquidGlass appearance="class" interactive swell /> |
| 614 | <SearchIcon /> |
| 615 | <input className="bg-transparent outline-none" placeholder="Search" /> |
| 616 | </label> |
| 617 | ``` |
| 618 |
| 619 | ## A glass card |
| 620 |
| 621 | Regular glass on a rounded box: the rim bends what crosses it, and the body keeps the card's text legible. |
| 622 |
| 623 | ```tsx |
| 624 | import { LiquidGlass } from "@colineapp/liquid-glass/react"; |
| 625 | import "@colineapp/liquid-glass/styles.css"; |
| 626 |
| 627 | export function Card() { |
| 628 | return ( |
| 629 | <div className="relative grid gap-1 rounded-[30px] p-5"> |
| 630 | <LiquidGlass appearance="class" /> |
| 631 | <h3 className="font-semibold">Now playing</h3> |
| 632 | <p className="text-sm opacity-70">Glass takes the box's radius and follows its size.</p> |
| 633 | </div> |
| 634 | ); |
| 635 | } |
| 636 | ``` |
| 637 |