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>
  );
}