SVG Liquid Distortion

Organic distortion using SVG feTurbulence and feDisplacementMap filters. No WebGL — works everywhere, animates with SMIL.

Preview

Loading preview…

Customize

Scale30
Frequency0.015
Animate
Radius16
"use client";

import { useId } from "react";
import styles from "./SvgLiquidDistortion.module.css";

type SvgLiquidDistortionProps = {
  scale?: number;
  frequency?: number;
  animate?: boolean;
  radius?: number;
  className?: string;
  children: React.ReactNode;
};

export default function SvgLiquidDistortion({
  scale = 30,
  frequency = 0.015,
  animate = true,
  radius = 16,
  className,
  children,
}: SvgLiquidDistortionProps) {
  const id = useId().replace(/:/g, "");
  const filterId = `liquid-${id}`;

  const freqStr = `${frequency} ${frequency}`;
  const freqAnim = `${frequency} ${frequency};${frequency * 1.4} ${frequency * 1.2};${frequency} ${frequency}`;

  return (
    <div className={[styles.wrapper, className].filter(Boolean).join(" ")} style={{ borderRadius: radius, overflow: "hidden" }}>
      <svg className={styles.svg} aria-hidden="true">
        <defs>
          <filter id={filterId} x="-10%" y="-10%" width="120%" height="120%">
            <feTurbulence
              type="fractalNoise"
              baseFrequency={freqStr}
              numOctaves={3}
              result="noise"
            >
              {animate && (
                <animate
                  attributeName="baseFrequency"
                  values={freqAnim}
                  dur="6s"
                  repeatCount="indefinite"
                />
              )}
            </feTurbulence>
            <feDisplacementMap
              in="SourceGraphic"
              in2="noise"
              scale={scale}
              xChannelSelector="R"
              yChannelSelector="G"
            />
          </filter>
        </defs>
      </svg>
      <div
        className={styles.content}
        style={{ filter: `url(#${filterId})` }}
      >
        {children}
      </div>
    </div>
  );
}