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.
| import { IconBrandNpm, IconDeviceDesktop, IconFileText, IconMoon, IconRobot, IconSun } from "@tabler/icons-react"; |
| 5 | import { |
| 6 | Badge, |
| 7 | Button, |
| 8 | Card, |
| 9 | CardContent, |
| 10 | CardDescription, |
| 11 | CardFooter, |
| 12 | CardHeader, |
| 13 | CardTitle, |
| 14 | ColineSiteProvider, |
| 15 | EmptyState, |
| 16 | SegmentedControl, |
| 17 | Separator, |
| 18 | Tabs, |
| 19 | TabsContent, |
| 20 | TabsList, |
| 21 | TabsTrigger, |
| 22 | type ColineSiteTheme, |
| 23 | } from "@colineapp/ui"; |
| 24 | import { LiquidGlass } from "@colineapp/liquid-glass/react"; |
| 25 | import "@colineapp/ui/styles.css"; |
| 26 | import "@colineapp/liquid-glass/styles.css"; |
| 27 | import "./app.css"; |
| 28 | import { EXAMPLES, INSTALL, NPM_URL, PACKAGE, SITE_URL, VERSION } from "./api"; |
| 29 | import { CodeBlock, CopyButton, InstallCommand } from "./code"; |
| 30 | import { Playground } from "./playground"; |
| 31 | import { Reference, Rules } from "./reference"; |
| 32 | import { GlassDialog, GlassLens, GlassMenu, GlassSearch, GlassTabs, GlassToolbar } from "./showcase"; |
| 33 | import { Stage, type BackdropKind } from "./stage"; |
| 34 |
| 35 | // The @colineapp/liquid-glass site: the page chrome is Coline's own |
| 36 | // components (@colineapp/ui); everything made of glass is the package, |
| 37 | // installed from npm like anyone else would. app.config.ts lists the plain-text |
| 38 | // docs for agents and the two tools agents in the browser can call. |
| 39 |
| 40 | const ACCENT = "#5b5bd6"; |
| 41 | const THEME_KEY = "liquid-glass-theme"; |
| 42 |
| 43 | const example = (id: string) => EXAMPLES.find((item) => item.id === id)?.code ?? ""; |
| 44 |
| 45 | const NAV = [ |
| 46 | { href: "#playground", label: "Playground" }, |
| 47 | { href: "#components", label: "Components" }, |
| 48 | { href: "#install", label: "Install" }, |
| 49 | { href: "#reference", label: "Reference" }, |
| 50 | { href: "#agents", label: "For agents" }, |
| 51 | ]; |
| 52 |
| 53 | function readTheme(): ColineSiteTheme { |
| 54 | try { |
| 55 | const saved = localStorage.getItem(THEME_KEY); |
| 56 | return saved === "light" || saved === "dark" ? saved : "system"; |
| 57 | } catch { |
| 58 | return "system"; |
| 59 | } |
| 60 | } |
| 61 |
| 62 | function Mark() { |
| 63 | return ( |
| 64 | <span className="relative grid size-8 place-items-center rounded-full" aria-hidden="true"> |
| 65 | <LiquidGlass appearance="class" /> |
| 66 | <span className="size-3 rounded-full bg-action" /> |
| 67 | </span> |
| 68 | ); |
| 69 | } |
| 70 |
| 71 | function Header({ theme, onTheme }: { theme: ColineSiteTheme; onTheme: (theme: ColineSiteTheme) => void }) { |
| 72 | return ( |
| 73 | <header className="sticky top-3 z-50 px-3 sm:px-4"> |
| 74 | <nav className="relative mx-auto flex h-14 max-w-6xl items-center gap-2 rounded-full pr-2 pl-3 text-foreground" aria-label="Main"> |
| 75 | <LiquidGlass appearance="class" interactive /> |
| 76 | <a href="#top" className="flex items-center gap-2.5 pr-2 font-semibold tracking-tight"> |
| 77 | <Mark /> |
| 78 | <span>Liquid Glass</span> |
| 79 | </a> |
| 80 | <div className="hidden flex-1 items-center gap-0.5 md:flex"> |
| 81 | {NAV.map((item) => ( |
| 82 | <a |
| 83 | key={item.href} |
| 84 | href={item.href} |
| 85 | className="rounded-full px-3 py-1.5 text-sm font-medium text-foreground/65 transition-colors hover:text-foreground" |
| 86 | > |
| 87 | {item.label} |
| 88 | </a> |
| 89 | ))} |
| 90 | </div> |
| 91 | <div className="ml-auto flex items-center gap-1.5"> |
| 92 | <SegmentedControl |
| 93 | label="Appearance" |
| 94 | size="sm" |
| 95 | value={theme} |
| 96 | onValueChange={onTheme} |
| 97 | options={[ |
| 98 | { value: "system", label: <IconDeviceDesktop className="size-4" aria-label="System" /> }, |
| 99 | { value: "light", label: <IconSun className="size-4" aria-label="Light" /> }, |
| 100 | { value: "dark", label: <IconMoon className="size-4" aria-label="Dark" /> }, |
| 101 | ]} |
| 102 | /> |
| 103 | <Button asChild variant="ghost" size="icon-sm" className="hidden sm:inline-flex"> |
| 104 | <a href={NPM_URL} aria-label="npm"> |
| 105 | <IconBrandNpm className="size-5" /> |
| 106 | </a> |
| 107 | </Button> |
| 108 | </div> |
| 109 | </nav> |
| 110 | </header> |
| 111 | ); |
| 112 | } |
| 113 |
| 114 | function Hero() { |
| 115 | const [tab, setTab] = React.useState("Library"); |
| 116 | return ( |
| 117 | <section id="top" className="mx-auto grid w-full max-w-6xl gap-10 px-4 pt-16 sm:px-6 sm:pt-24"> |
| 118 | <div className="grid justify-items-center gap-6 text-center"> |
| 119 | <Badge variant="secondary"> |
| 120 | {PACKAGE} {VERSION} · MIT |
| 121 | </Badge> |
| 122 | <h1 className="max-w-3xl text-[clamp(40px,7vw,76px)] leading-[1.02] font-semibold tracking-[-0.035em] text-balance"> |
| 123 | Liquid Glass for the web |
| 124 | </h1> |
| 125 | <p className="max-w-2xl text-lg leading-relaxed text-pretty text-muted-foreground"> |
| 126 | The rim refraction, light and motion of iOS 27's glass, on any rounded element. With React bindings and pill |
| 127 | tabs whose thumb lifts into a lens. |
| 128 | </p> |
| 129 | <div className="flex flex-wrap items-center justify-center gap-3"> |
| 130 | <InstallCommand command={INSTALL} /> |
| 131 | <Button asChild size="lg"> |
| 132 | <a href="#playground">Open the playground</a> |
| 133 | </Button> |
| 134 | </div> |
| 135 | </div> |
| 136 | <Stage backdrop="landscape" className="h-[520px] sm:h-[600px]"> |
| 137 | <div className="absolute inset-x-0 top-6 flex justify-center px-4"> |
| 138 | <GlassTabs label="Photos" options={["Library", "Albums", "People", "Places"]} value={tab} onChange={setTab} /> |
| 139 | </div> |
| 140 | <div className="absolute inset-x-0 top-[38%] flex justify-center px-6"> |
| 141 | <GlassSearch className="w-full max-w-md" placeholder={`Search ${tab.toLowerCase()}`} /> |
| 142 | </div> |
| 143 | <GlassLens size={156} initial={{ x: 0.14, y: 0.78 }} /> |
| 144 | <div className="absolute bottom-6 left-1/2 hidden -translate-x-1/2 sm:block"> |
| 145 | <GlassToolbar /> |
| 146 | </div> |
| 147 | </Stage> |
| 148 | <p className="-mt-6 text-center text-sm text-muted-foreground"> |
| 149 | Press and drag the tabs, press the search field, and drag the lens. Everything here is the package. |
| 150 | </p> |
| 151 | </section> |
| 152 | ); |
| 153 | } |
| 154 |
| 155 | function SectionHeading({ id, title, children }: { id: string; title: string; children: React.ReactNode }) { |
| 156 | return ( |
| 157 | <div className="grid max-w-2xl gap-2"> |
| 158 | <h2 id={`${id}-title`} className="text-3xl font-semibold tracking-tight"> |
| 159 | {title} |
| 160 | </h2> |
| 161 | <p className="leading-relaxed text-muted-foreground">{children}</p> |
| 162 | </div> |
| 163 | ); |
| 164 | } |
| 165 |
| 166 | function Section({ id, title, intro, children }: { id: string; title: string; intro: React.ReactNode; children: React.ReactNode }) { |
| 167 | return ( |
| 168 | <section id={id} aria-labelledby={`${id}-title`} className="mx-auto grid w-full max-w-6xl scroll-mt-24 gap-8 px-4 sm:px-6"> |
| 169 | <SectionHeading id={id} title={title}> |
| 170 | {intro} |
| 171 | </SectionHeading> |
| 172 | {children} |
| 173 | </section> |
| 174 | ); |
| 175 | } |
| 176 |
| 177 | function DemoCard({ |
| 178 | title, |
| 179 | description, |
| 180 | code, |
| 181 | backdrop, |
| 182 | children, |
| 183 | }: { |
| 184 | title: string; |
| 185 | description: string; |
| 186 | code: string; |
| 187 | backdrop: BackdropKind; |
| 188 | children: React.ReactNode; |
| 189 | }) { |
| 190 | return ( |
| 191 | <Card className="overflow-hidden"> |
| 192 | <CardHeader> |
| 193 | <CardTitle>{title}</CardTitle> |
| 194 | <CardDescription>{description}</CardDescription> |
| 195 | </CardHeader> |
| 196 | <CardContent> |
| 197 | <Stage backdrop={backdrop} className="h-72"> |
| 198 | {children} |
| 199 | </Stage> |
| 200 | </CardContent> |
| 201 | <CardFooter className="justify-end"> |
| 202 | <CopyButton text={code} label="Copy code" /> |
| 203 | </CardFooter> |
| 204 | </Card> |
| 205 | ); |
| 206 | } |
| 207 |
| 208 | function Components() { |
| 209 | const [tab, setTab] = React.useState("Week"); |
| 210 | return ( |
| 211 | <Section id="components" title="Components" intro="The surfaces you'd build with it, made from the package the way its docs describe. Each card copies its code."> |
| 212 | <div className="grid gap-5 md:grid-cols-2"> |
| 213 | <DemoCard title="Pill tabs" description="The thumb lifts into a lens, stretches, magnifies the labels and squashes as it lands. Drag the selected tab." code={example("tabs")} backdrop="night"> |
| 214 | <div className="absolute inset-0 grid place-items-center px-4"> |
| 215 | <GlassTabs label="Period" options={["Day", "Week", "Month", "Year"]} value={tab} onChange={setTab} /> |
| 216 | </div> |
| 217 | </DemoCard> |
| 218 | <DemoCard title="Menu" description="Floating glass that materializes from clear as it opens, with a well under the item you point at." code={example("menu")} backdrop="landscape"> |
| 219 | <div className="absolute top-8 left-8"> |
| 220 | <GlassMenu /> |
| 221 | </div> |
| 222 | </DemoCard> |
| 223 | <DemoCard title="Dialog" description={'floating="dialog" diffuses the page more and casts a deeper shadow.'} code={example("dialog")} backdrop="clouds"> |
| 224 | <GlassDialog /> |
| 225 | </DemoCard> |
| 226 | <DemoCard title="Search field" description="interactive lights the glass under the pointer; swell springs it when pressed, like the tab bar." code={example("search")} backdrop="landscape"> |
| 227 | <div className="absolute inset-0 grid place-items-center px-6"> |
| 228 | <GlassSearch className="w-full max-w-sm" /> |
| 229 | </div> |
| 230 | </DemoCard> |
| 231 | <DemoCard title="Lens" description="The tabs' lens on its own: four samples a pixel and magnify(). Drag it across the text." code={example("lens")} backdrop="page"> |
| 232 | <GlassLens size={120} initial={{ x: 0.3, y: 0.3 }} /> |
| 233 | </DemoCard> |
| 234 | <DemoCard title="Toolbar" description="Controls on glass get no glass of their own: the pressed ones are wells in it." code={example("react")} backdrop="night"> |
| 235 | <div className="absolute inset-0 grid place-items-center px-4"> |
| 236 | <GlassToolbar /> |
| 237 | </div> |
| 238 | </DemoCard> |
| 239 | </div> |
| 240 | </Section> |
| 241 | ); |
| 242 | } |
| 243 |
| 244 | function Install() { |
| 245 | return ( |
| 246 | <Section id="install" title="Install" intro={<>One package, no dependencies, with React bindings under <code className="font-mono text-[0.9em]">/react</code>. Import the stylesheet once.</>}> |
| 247 | <div className="grid gap-6"> |
| 248 | <div> |
| 249 | <InstallCommand command={INSTALL} /> |
| 250 | </div> |
| 251 | <Tabs defaultValue="react"> |
| 252 | <TabsList> |
| 253 | <TabsTrigger value="react">React</TabsTrigger> |
| 254 | <TabsTrigger value="vanilla">TypeScript</TabsTrigger> |
| 255 | <TabsTrigger value="tabs">Tabs</TabsTrigger> |
| 256 | <TabsTrigger value="menu">Menu</TabsTrigger> |
| 257 | </TabsList> |
| 258 | {["react", "vanilla", "tabs", "menu"].map((id) => { |
| 259 | const item = EXAMPLES.find((entry) => entry.id === id); |
| 260 | return item ? ( |
| 261 | <TabsContent key={id} value={id}> |
| 262 | <CodeBlock title={item.description} code={item.code} /> |
| 263 | </TabsContent> |
| 264 | ) : null; |
| 265 | })} |
| 266 | </Tabs> |
| 267 | <div className="grid gap-3"> |
| 268 | <h3 className="text-lg font-semibold tracking-tight">Six rules for the surface</h3> |
| 269 | <Rules /> |
| 270 | </div> |
| 271 | </div> |
| 272 | </Section> |
| 273 | ); |
| 274 | } |
| 275 |
| 276 | const AGENT_PROMPT = `Add Liquid Glass to the nav bar with ${PACKAGE}. Read ${SITE_URL}/agents.md first and follow its rules.`; |
| 277 |
| 278 | const AGENT_FILES = [ |
| 279 | { href: "/llms.txt", title: "llms.txt", description: "The index: every doc and option, one link each." }, |
| 280 | { href: "/agents.md", title: "agents.md", description: "How to use the package, which options suit which surface, and the rules that keep the glass working." }, |
| 281 | { href: "/docs/api.md", title: "docs/api.md", description: "The whole reference in Markdown." }, |
| 282 | { href: "/llms-full.txt", title: "llms-full.txt", description: "Reference and examples in one file." }, |
| 283 | { href: "/options.json", title: "options.json", description: "The reference as JSON, for tools." }, |
| 284 | { href: "/docs/options/swell.md", title: "docs/options/*.md", description: "One plain-text page per option, geometry field and method." }, |
| 285 | ]; |
| 286 |
| 287 | function Agents() { |
| 288 | return ( |
| 289 | <Section id="agents" title="For agents" intro="Everything on this page is also plain text, so a coding agent can use the package without reading its source."> |
| 290 | <div className="grid gap-5 lg:grid-cols-[minmax(0,1fr)_380px]"> |
| 291 | <Card> |
| 292 | <CardHeader> |
| 293 | <CardTitle>Plain-text docs</CardTitle> |
| 294 | <CardDescription>Served from this site, generated from the same reference as the page.</CardDescription> |
| 295 | </CardHeader> |
| 296 | <CardContent> |
| 297 | <ul className="divide-y divide-border"> |
| 298 | {AGENT_FILES.map((file) => ( |
| 299 | <li key={file.href} className="flex items-start gap-3 py-3 first:pt-0 last:pb-0"> |
| 300 | <IconFileText className="mt-0.5 size-4 shrink-0 text-muted-foreground" /> |
| 301 | <div className="grid gap-0.5"> |
| 302 | <a href={file.href} className="font-mono text-sm font-medium text-action hover:underline"> |
| 303 | {file.title} |
| 304 | </a> |
| 305 | <span className="text-sm text-muted-foreground">{file.description}</span> |
| 306 | </div> |
| 307 | </li> |
| 308 | ))} |
| 309 | </ul> |
| 310 | </CardContent> |
| 311 | </Card> |
| 312 | <div className="grid content-start gap-5"> |
| 313 | <Card> |
| 314 | <CardHeader> |
| 315 | <CardTitle>Hand it to your agent</CardTitle> |
| 316 | <CardDescription>Paste this into Claude Code, Codex, Cursor or any agent.</CardDescription> |
| 317 | </CardHeader> |
| 318 | <CardContent> |
| 319 | <CodeBlock code={AGENT_PROMPT} title="Prompt" wrap /> |
| 320 | </CardContent> |
| 321 | </Card> |
| 322 | <Card> |
| 323 | <CardHeader> |
| 324 | <CardTitle className="flex items-center gap-2"> |
| 325 | <IconRobot className="size-4" /> |
| 326 | Agents in the browser |
| 327 | </CardTitle> |
| 328 | <CardDescription> |
| 329 | This page offers two read-only tools over WebMCP: <code className="font-mono">liquid-glass.reference</code> looks up |
| 330 | any option, and <code className="font-mono">liquid-glass.snippet</code> writes the code for a set of options. |
| 331 | </CardDescription> |
| 332 | </CardHeader> |
| 333 | </Card> |
| 334 | </div> |
| 335 | </div> |
| 336 | </Section> |
| 337 | ); |
| 338 | } |
| 339 |
| 340 | function Footer() { |
| 341 | return ( |
| 342 | <footer className="mx-auto grid w-full max-w-6xl gap-6 px-4 pb-16 sm:px-6"> |
| 343 | <Separator /> |
| 344 | <div className="flex flex-wrap items-center justify-between gap-4 text-sm text-muted-foreground"> |
| 345 | <span> |
| 346 | {PACKAGE} {VERSION}, MIT. Made by Coline. |
| 347 | </span> |
| 348 | <div className="flex flex-wrap gap-1"> |
| 349 | <Button asChild variant="ghost" size="sm"> |
| 350 | <a href={NPM_URL}>npm</a> |
| 351 | </Button> |
| 352 | <Button asChild variant="ghost" size="sm"> |
| 353 | <a href="/llms.txt">llms.txt</a> |
| 354 | </Button> |
| 355 | </div> |
| 356 | </div> |
| 357 | </footer> |
| 358 | ); |
| 359 | } |
| 360 |
| 361 | function App() { |
| 362 | const [theme, setTheme] = React.useState<ColineSiteTheme>(readTheme); |
| 363 | const changeTheme = (next: ColineSiteTheme) => { |
| 364 | setTheme(next); |
| 365 | try { |
| 366 | localStorage.setItem(THEME_KEY, next); |
| 367 | } catch { |
| 368 | // The choice lasts this visit. |
| 369 | } |
| 370 | }; |
| 371 | return ( |
| 372 | <ColineSiteProvider theme={theme} accent={ACCENT}> |
| 373 | <div className="min-h-dvh bg-background text-foreground"> |
| 374 | <Header theme={theme} onTheme={changeTheme} /> |
| 375 | <main className="grid gap-28 pb-24 sm:gap-36"> |
| 376 | <Hero /> |
| 377 | <Section |
| 378 | id="playground" |
| 379 | title="Playground" |
| 380 | intro="Every option, live. Drag the glass across the picture, change the rim, and copy the code for what you made." |
| 381 | > |
| 382 | <Playground /> |
| 383 | </Section> |
| 384 | <Components /> |
| 385 | <Install /> |
| 386 | <Section id="reference" title="Reference" intro={`Every option, method and export of ${PACKAGE}. Each name links to its plain-text page.`}> |
| 387 | <Reference /> |
| 388 | </Section> |
| 389 | <Agents /> |
| 390 | </main> |
| 391 | <Footer /> |
| 392 | </div> |
| 393 | </ColineSiteProvider> |
| 394 | ); |
| 395 | } |
| 396 |
| 397 | createRoot(document.getElementById("root")!).render( |
| 398 | <React.StrictMode> |
| 399 | <SiteErrorBoundary |
| 400 | fallback={(_error, retry) => ( |
| 401 | <ColineSiteProvider> |
| 402 | <main className="mx-auto max-w-xl px-5 py-16"> |
| 403 | <EmptyState |
| 404 | title="Something went wrong" |
| 405 | action={ |
| 406 | <Button variant="outline" onClick={retry}> |
| 407 | Try again |
| 408 | </Button> |
| 409 | } |
| 410 | /> |
| 411 | </main> |
| 412 | </ColineSiteProvider> |
| 413 | )} |
| 414 | > |
| 415 | <App /> |
| 416 | </SiteErrorBoundary> |
| 417 | </React.StrictMode>, |
| 418 | ); |
| 419 |
| 420 |