Liquid Glass Button
Apple-style liquid glass button with SVG displacement refraction, chromatic aberration, and frosted glass effect
buttonglassglassmorphismliquiddisplacementrefractionapple
Preview
Source Code
"use client";
import { useEffect, useId, useMemo, useRef, useState } from "react";
interface LiquidButtonProps {
children?: React.ReactNode;
scale?: number;
radius?: number;
border?: number;
lightness?: number;
displace?: number;
alpha?: number;
blur?: number;
dispersion?: number;
frost?: number;
borderColor?: string;
className?: string;
onClick?: () => void;
}
export default function LiquidButton({
children = "Liquid Glass Button",
scale = 160,
radius = 50,
border = 0.05,
lightness = 53,
displace = 0.38,
alpha = 0.9,
blur = 5,
dispersion = 50,
frost = 0.1,
borderColor = "rgba(120, 120, 120, 0.7)",
className = "",
onClick,
}: LiquidButtonProps) {
const uniqueId = useId().replace(/:/g, "");
const filterId = `liquid-glass-filter-${uniqueId}`;
const containerRef = useRef<HTMLButtonElement>(null);
const [dimensions, setDimensions] = useState({ width: 200, height: 60 });
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const update = () => {
const { width, height } = el.getBoundingClientRect();
if (width > 0 && height > 0) setDimensions({ width, height });
};
update();
const ro = new ResizeObserver(update);
ro.observe(el);
return () => ro.disconnect();
}, []);
const displacementDataUri = useMemo(() => {
const { width, height } = dimensions;
const w = width / 2;
const h = height / 2;
const borderSize = Math.min(w, h) * (border * 0.5);
const effectiveRadius = Math.min(radius, width / 2, height / 2);
const svgContent = `<svg viewBox="0 0 ${w} ${h}" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="red" x1="100%" y1="0%" x2="0%" y2="0%"><stop offset="0%" stop-color="#0000"/><stop offset="100%" stop-color="red"/></linearGradient><linearGradient id="blue" x1="0%" y1="0%" x2="0%" y2="100%"><stop offset="0%" stop-color="#0000"/><stop offset="100%" stop-color="blue"/></linearGradient></defs><rect x="0" y="0" width="${w}" height="${h}" fill="black"/><rect x="0" y="0" width="${w}" height="${h}" rx="${effectiveRadius}" fill="url(#red)"/><rect x="0" y="0" width="${w}" height="${h}" rx="${effectiveRadius}" fill="url(#blue)" style="mix-blend-mode:difference"/><rect x="${borderSize}" y="${borderSize}" width="${w - borderSize * 2}" height="${h - borderSize * 2}" rx="${effectiveRadius}" fill="hsl(0 0% ${lightness}% / ${alpha})" style="filter:blur(${blur}px)"/></svg>`;
return `data:image/svg+xml,${encodeURIComponent(svgContent)}`;
}, [dimensions, border, radius, lightness, alpha, blur]);
return (
<button
ref={containerRef}
onClick={onClick}
className={className}
style={{
position: "relative",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
padding: "14px 36px",
background: "none",
border: "none",
cursor: "pointer",
outline: "none",
borderRadius: radius,
}}
>
{/* Glass effect layer with SVG displacement filter */}
<div
style={{
position: "absolute",
inset: 0,
borderRadius: radius,
zIndex: 1,
background: `hsl(0 0% 100% / ${frost})`,
backdropFilter: `url(#${filterId})`,
WebkitBackdropFilter: `url(#${filterId})`,
}}
>
<svg
style={{
width: "100%",
height: "100%",
pointerEvents: "none",
position: "absolute",
inset: 0,
}}
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<filter
id={filterId}
colorInterpolationFilters="sRGB"
>
<feImage
href={displacementDataUri}
x="0"
y="0"
width="100%"
height="100%"
result="map"
/>
{/* Red channel displacement */}
<feDisplacementMap
in="SourceGraphic"
in2="map"
scale={scale + dispersion}
xChannelSelector="R"
yChannelSelector="B"
result="dispRed"
/>
<feColorMatrix
in="dispRed"
type="matrix"
values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"
result="red"
/>
{/* Green channel displacement */}
<feDisplacementMap
in="SourceGraphic"
in2="map"
scale={scale + dispersion}
xChannelSelector="R"
yChannelSelector="B"
result="dispGreen"
/>
<feColorMatrix
in="dispGreen"
type="matrix"
values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0"
result="green"
/>
{/* Blue channel displacement */}
<feDisplacementMap
in="SourceGraphic"
in2="map"
scale={scale + dispersion}
xChannelSelector="R"
yChannelSelector="B"
result="dispBlue"
/>
<feColorMatrix
in="dispBlue"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0"
result="blue"
/>
{/* Combine channels */}
<feBlend in="red" in2="green" mode="screen" result="rg" />
<feBlend in="rg" in2="blue" mode="screen" result="output" />
<feGaussianBlur in="output" stdDeviation={displace} />
</filter>
</defs>
</svg>
</div>
{/* Gradient border */}
<div
style={{
position: "absolute",
inset: 0,
borderRadius: radius,
zIndex: 2,
pointerEvents: "none",
background: `linear-gradient(315deg, ${borderColor} 0%, rgba(120, 120, 120, 0) 30%, rgba(120, 120, 120, 0) 70%, ${borderColor} 100%) border-box`,
mask: "linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0)",
WebkitMask:
"linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0)",
maskComposite: "exclude",
WebkitMaskComposite: "xor",
border: "1px solid transparent",
}}
/>
{/* Content */}
<span
style={{
position: "relative",
zIndex: 3,
color: "rgba(255, 255, 255, 0.85)",
fontSize: 15,
fontWeight: 500,
letterSpacing: "0.01em",
whiteSpace: "nowrap",
textShadow: "0 0.5px 1px rgba(0, 0, 0, 0.15)",
}}
>
{children}
</span>
</button>
);
}