Liquid Glass Sphere
Apple & Figma-style liquid glass sphere with GLSL refraction, animated color blobs, and organic noise distortion. Pure WebGL — no Three.js required.
Preview
Loading preview…
Customize
Size240
Speed1
IOR1.45
Shape
"use client";
import { useRef, useEffect, useCallback } from "react";
import styles from "./LiquidGlassSphere.module.css";
type Shape = "circle" | "square";
interface LiquidGlassSphereProps {
size?: number; // px, default 240
speed?: number; // time multiplier, default 1.0
ior?: number; // index of refraction, default 1.45
shape?: Shape; // "circle" or "square", default "circle"
className?: string;
}
// Vertex shader: full-screen quad
const VERT = `
attribute vec2 position;
void main() { gl_Position = vec4(position, 0.0, 1.0); }
`;
// Fragment shader: see shader.glsl tab for full source
const FRAG = `/* ... */`;
export default function LiquidGlassSphere({
size = 240, speed = 1, ior = 1.45, shape = "circle", className,
}: LiquidGlassSphereProps) {
const sphereRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const glRef = useRef<{ /* WebGL context + uniforms */ } | null>(null);
const speedRef = useRef(speed);
const iorRef = useRef(ior);
const shapeRef = useRef(shape);
// Update refs without re-initializing WebGL
useEffect(() => { speedRef.current = speed; }, [speed]);
useEffect(() => { iorRef.current = ior; }, [ior]);
useEffect(() => { shapeRef.current = shape; }, [shape]);
const setup = useCallback(() => {
// Create canvas, compile shaders, link program
// Store uniform locations: uResolution, uTime, uIOR, uShape
}, []);
useEffect(() => {
setup();
const ctx = glRef.current;
if (!ctx) return;
const t0 = performance.now();
// 30fps render loop
const loop = (now: number) => {
ctx.gl.uniform1f(ctx.uTime,
((now - t0) / 1000) * speedRef.current);
ctx.gl.uniform1f(ctx.uIOR, iorRef.current);
ctx.gl.uniform1f(ctx.uShape,
shapeRef.current === "square" ? 1.0 : 0.0);
ctx.gl.drawArrays(ctx.gl.TRIANGLE_STRIP, 0, 4);
requestAnimationFrame(loop);
};
// IntersectionObserver to pause when off-screen
// prefers-reduced-motion: render single frame
// Cleanup: loseContext, cancelAnimationFrame
return () => { /* cleanup */ };
}, [setup]);
const borderRadius = shape === "square" ? size * 0.12 : "50%";
return (
<div ref={sphereRef} className={styles.sphere}
style={{ width: size, height: size, borderRadius }}>
<div ref={containerRef} className={styles.canvas}
style={{ borderRadius }} />
</div>
);
}