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