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.
| 6 | export type PlaygroundOptions = { |
| 7 | variant?: "regular" | "clear" | undefined; |
| 8 | level?: number | undefined; |
| 9 | floating?: boolean | "dialog" | undefined; |
| 10 | interactive?: boolean | undefined; |
| 11 | swell?: boolean | undefined; |
| 12 | hue?: string | null | undefined; |
| 13 | hueStrength?: number | undefined; |
| 14 | refraction?: boolean | undefined; |
| 15 | samples?: 1 | 4 | undefined; |
| 16 | geometry?: { maxFall?: number; bezel?: number; thickness?: number; edge?: number; glint?: number } | undefined; |
| 17 | shape?: "capsule" | "card" | "circle" | undefined; |
| 18 | }; |
| 19 |
| 20 | /** Defaults the snippet leaves out. */ |
| 21 | const DEFAULTS = { maxFall: 0.45, bezel: 17, thickness: 24, edge: 0.75, glint: 0.4 } as const; |
| 22 | const DEFAULT_LEVEL = 0.5; |
| 23 |
| 24 | const round = (value: number) => Number(value.toFixed(2)); |
| 25 |
| 26 | function geometryEntries(options: PlaygroundOptions) { |
| 27 | const geometry = options.geometry ?? {}; |
| 28 | return (Object.keys(DEFAULTS) as (keyof typeof DEFAULTS)[]) |
| 29 | .filter(key => geometry[key] !== undefined && round(geometry[key]!) !== DEFAULTS[key]) |
| 30 | .map(key => [key, round(geometry[key]!)] as const); |
| 31 | } |
| 32 |
| 33 | /** The options that differ from the defaults, as [name, JSX value, object value]. */ |
| 34 | function entries(options: PlaygroundOptions): [string, string, string][] { |
| 35 | const out: [string, string, string][] = [["appearance", '"class"', '"class"']]; |
| 36 | if (options.variant === "clear") out.push(["variant", '"clear"', '"clear"']); |
| 37 | if (options.variant !== "clear" && options.level !== undefined && round(options.level) !== DEFAULT_LEVEL) { |
| 38 | out.push(["level", `{${round(options.level)}}`, String(round(options.level))]); |
| 39 | } |
| 40 | if (options.floating === "dialog") out.push(["floating", '"dialog"', '"dialog"']); |
| 41 | else if (options.floating) out.push(["floating", "", "true"]); |
| 42 | if (options.interactive) out.push(["interactive", "", "true"]); |
| 43 | // Interactive glass swells unless told not to. |
| 44 | const swell = options.swell ?? Boolean(options.interactive); |
| 45 | if (swell !== Boolean(options.interactive)) out.push(["swell", swell ? "" : "{false}", String(swell)]); |
| 46 | if (options.hue) out.push(["hue", `"${options.hue}"`, `"${options.hue}"`]); |
| 47 | if (options.hue && options.hueStrength !== undefined) out.push(["hueStrength", `{${round(options.hueStrength)}}`, String(round(options.hueStrength))]); |
| 48 | if (options.refraction === false) out.push(["refraction", "{false}", "false"]); |
| 49 | if (options.samples === 1) out.push(["samples", "{1}", "1"]); |
| 50 | const geometry = geometryEntries(options); |
| 51 | if (geometry.length) { |
| 52 | const literal = `{ ${geometry.map(([key, value]) => `${key}: ${value}`).join(", ")} }`; |
| 53 | out.push(["geometry", `{${literal}}`, literal]); |
| 54 | } |
| 55 | return out; |
| 56 | } |
| 57 |
| 58 | /** JSX props for the options, in a stable order. */ |
| 59 | export function jsxProps(options: PlaygroundOptions) { |
| 60 | return entries(options).map(([name, value]) => (value ? `${name}=${value}` : name)); |
| 61 | } |
| 62 |
| 63 | function objectLiteral(options: PlaygroundOptions) { |
| 64 | return entries(options).map(([name, , value]) => `${name}: ${value}`); |
| 65 | } |
| 66 |
| 67 | const SHAPE_CLASSES = { |
| 68 | capsule: "relative rounded-full px-6 py-3", |
| 69 | card: "relative rounded-[32px] p-6", |
| 70 | circle: "relative size-40 rounded-full", |
| 71 | } as const; |
| 72 |
| 73 | export function reactSnippet(options: PlaygroundOptions) { |
| 74 | const props = jsxProps(options); |
| 75 | const glass = props.join(" ").length > 56 ? `<LiquidGlass\n ${props.join("\n ")}\n />` : `<LiquidGlass ${props.join(" ")} />`; |
| 76 | return `import { LiquidGlass } from "${PACKAGE}/react"; |
| 77 | import "${PACKAGE}/styles.css"; |
| 78 |
| 79 | <div className="${SHAPE_CLASSES[options.shape ?? "capsule"]}"> |
| 80 | ${glass} |
| 81 | … |
| 82 | </div>`; |
| 83 | } |
| 84 |
| 85 | export function vanillaSnippet(options: PlaygroundOptions) { |
| 86 | const literal = objectLiteral(options); |
| 87 | return `import { createLiquidGlass } from "${PACKAGE}"; |
| 88 | import "${PACKAGE}/styles.css"; |
| 89 |
| 90 | const glass = createLiquidGlass(element, { |
| 91 | ${literal.join(",\n ")}, |
| 92 | });`; |
| 93 | } |
| 94 |