Docs
Liquid Wave

Liquid Wave

A responsive, interactive wave animation with customizable properties.

Calm Ocean Waves

Energetic Waves

Subtle Background Wave

Installation

Copy and paste the following code into your project.

"use client";
 
import React, { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
 
export interface LiquidWaveProps extends React.HTMLAttributes<HTMLDivElement> {
  height?: number;
  frequency?: number;
  amplitude?: number;
  speed?: number;
  color?: string;
  opacity?: number;
  layers?: number;
  showParticles?: boolean;
  particleCount?: number;
  particleColor?: string;
}
 
export function LiquidWave({
  className,
  height = 200,
  frequency = 3,
  amplitude = 30,
  speed = 0.3,
  color = "#2563eb",
  opacity = 0.5,
  layers = 3,
  showParticles = false,
  particleCount = 50,
  particleColor = "#ffffff",
  ...props
}: LiquidWaveProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [points, setPoints] = useState<{ x: number; y: number }[]>([]);
  const [particles, setParticles] = useState<
    { x: number; y: number; radius: number; speed: number }[]
  >([]);
  const frameRef = useRef<number>(0);
 
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
 
    const ctx = canvas.getContext("2d");
    if (!ctx) return;
 
    const resize = () => {
      canvas.width = canvas.offsetWidth * window.devicePixelRatio;
      canvas.height = height * window.devicePixelRatio;
      ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
    };
 
    const initPoints = () => {
      const points = [];
      const segments = Math.floor(canvas.offsetWidth / 50);
      for (let i = 0; i <= segments; i++) {
        points.push({
          x: (canvas.offsetWidth * i) / segments,
          y: height / 2,
        });
      }
      setPoints(points);
    };
 
    const initParticles = () => {
      if (!showParticles) return;
      const particles = [];
      for (let i = 0; i < particleCount; i++) {
        particles.push({
          x: Math.random() * canvas.offsetWidth,
          y: Math.random() * height,
          radius: Math.random() * 3 + 1,
          speed: Math.random() * 2 + 1,
        });
      }
      setParticles(particles);
    };
 
    resize();
    initPoints();
    initParticles();
 
    window.addEventListener("resize", () => {
      resize();
      initPoints();
      initParticles();
    });
 
    return () => {
      window.removeEventListener("resize", resize);
      cancelAnimationFrame(frameRef.current);
    };
  }, [height, showParticles, particleCount]);
 
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
 
    const ctx = canvas.getContext("2d");
    if (!ctx) return;
 
    const animate = () => {
      ctx.clearRect(0, 0, canvas.offsetWidth, height);
 
      // Draw multiple wave layers
      for (let l = 0; l < layers; l++) {
        // Create multiple wave frequencies for more organic motion
        const layerPoints = points.map((point) => {
          const time = Date.now() * 0.001 * speed;
          // Primary wave
          const wave1 =
            Math.sin(point.x * frequency * 0.01 + time + l) * amplitude;
          // Secondary wave with different frequency
          const wave2 =
            Math.sin(point.x * frequency * 0.02 + time * 1.5) *
            (amplitude * 0.5);
          // Micro wave for fine detail
          const wave3 =
            Math.sin(point.x * frequency * 0.05 + time * 0.7) *
            (amplitude * 0.2);
 
          return {
            ...point,
            y: height / 2 + (wave1 + wave2 + wave3) * (1 - l * 0.2),
          };
        });
 
        // Create wave path with smoother interpolation
        ctx.beginPath();
        ctx.moveTo(0, height);
 
        // Enhanced curve interpolation for smoother waves
        for (let i = 0; i < layerPoints.length - 1; i++) {
          const point = layerPoints[i];
          const nextPoint = layerPoints[i + 1];
 
          if (i === 0) {
            ctx.lineTo(point.x, point.y);
          }
 
          // Calculate control points for smoother curves
          const cx1 = point.x + (nextPoint.x - point.x) * 0.4;
          const cy1 = point.y;
          const cx2 = point.x + (nextPoint.x - point.x) * 0.6;
          const cy2 = nextPoint.y;
 
          // Use bezier curve for smoother wave motion
          ctx.bezierCurveTo(cx1, cy1, cx2, cy2, nextPoint.x, nextPoint.y);
        }
 
        ctx.lineTo(canvas.offsetWidth, height);
        ctx.lineTo(0, height);
        ctx.closePath();
 
        // Enhanced gradient with subtle color variations
        const gradient = ctx.createLinearGradient(0, 0, 0, height);
        const alpha = Math.round(opacity * 255)
          .toString(16)
          .padStart(2, "0");
 
        // Add subtle color variation based on layer
        const layerHue = l * 5; // Slight hue variation per layer
        const baseColor = adjustColor(color, layerHue);
 
        gradient.addColorStop(0, `${baseColor}${alpha}`);
        gradient.addColorStop(
          0.5,
          `${adjustColor(baseColor, 5)}${Math.round(opacity * 200)
            .toString(16)
            .padStart(2, "0")}`,
        );
        gradient.addColorStop(1, `${color}00`);
 
        ctx.fillStyle = gradient;
        ctx.fill();
      }
 
      // Draw particles with improved wave following
      if (showParticles) {
        ctx.fillStyle = particleColor;
        particles.forEach((particle) => {
          // More natural wave following motion
          const time = Date.now() * 0.001 * speed;
          const wave1 =
            Math.sin(particle.x * frequency * 0.01 + time) * amplitude;
          const wave2 =
            Math.sin(particle.x * frequency * 0.02 + time * 1.5) *
            (amplitude * 0.5);
          const targetY = height / 2 + wave1 + wave2;
 
          // Smooth particle movement
          particle.y += (targetY - particle.y) * 0.05;
          particle.x += particle.speed;
 
          if (particle.x > canvas.offsetWidth) {
            particle.x = 0;
            particle.y = Math.random() * height;
          }
 
          // Add subtle glow effect to particles
          const glow = ctx.createRadialGradient(
            particle.x,
            particle.y,
            0,
            particle.x,
            particle.y,
            particle.radius * 2,
          );
          glow.addColorStop(0, particleColor);
          glow.addColorStop(1, `${particleColor}00`);
 
          ctx.beginPath();
          ctx.fillStyle = glow;
          ctx.arc(particle.x, particle.y, particle.radius * 2, 0, Math.PI * 2);
          ctx.fill();
        });
      }
 
      frameRef.current = requestAnimationFrame(animate);
    };
 
    // Helper function to adjust color hue
    function adjustColor(color: string, amount: number): string {
      const hex = color.replace("#", "");
      const r = parseInt(hex.substring(0, 2), 16);
      const g = parseInt(hex.substring(2, 4), 16);
      const b = parseInt(hex.substring(4, 6), 16);
 
      // Simple hue adjustment
      const factor = 1 + amount / 100;
      return `#${Math.min(255, Math.round(r * factor))
        .toString(16)
        .padStart(2, "0")}${Math.min(255, Math.round(g * factor))
        .toString(16)
        .padStart(2, "0")}${Math.min(255, Math.round(b * factor))
        .toString(16)
        .padStart(2, "0")}`;
    }
 
    animate();
 
    return () => {
      cancelAnimationFrame(frameRef.current);
    };
  }, [
    points,
    particles,
    frequency,
    amplitude,
    speed,
    color,
    opacity,
    layers,
    showParticles,
    particleColor,
    height,
  ]);
 
  return (
    <div
      className={cn("relative w-full overflow-hidden", className)}
      {...props}
    >
      <canvas
        ref={canvasRef}
        style={{ height: `${height}px` }}
        className="w-full"
      />
    </div>
  );
}

Update the import paths to match your project setup.

import LiquidWave from "@/components/ui/liquid-wave";

Props

PropTypeDefaultDescription
heightnumber200Height of the wave container in pixels
frequencynumber3Wave frequency - higher values create more waves
amplitudenumber30Wave amplitude - controls wave height
speednumber0.3Animation speed - higher values make waves move faster
colorstring "#2563eb"Wave color in hex format
opacitynumber0.5Wave opacity (0-1)
layersnumber3Number of overlapping wave layers
showParticlesbooleanfalseEnable floating particles
particleCountnumber50Number of particles when enabled
particleColorstring"#ffffff"Color of particles in hex format
classNamestring-Additional CSS classes

Examples

Basic Wave

import LiquidWave from "@/components/ui/liquid-wave";
 
export default function BasicExample() {
  return <LiquidWave height={150} />;
}

Multiple Layers with Particles

import LiquidWave from "@/components/ui/liquid-wave";
 
export default function AdvancedExample() {
  return (
    <LiquidWave
      height={200}
      layers={4}
      frequency={2}
      amplitude={40}
      color="#8b5cf6"
      opacity={0.6}
      showParticles={true}
      particleCount={30}
      particleColor="#f0f0f0"
    />
  );
}

Energetic Wave

import LiquidWave from "@/components/ui/liquid-wave";
 
export default function EnergeticExample() {
  return (
    <LiquidWave
      height={120}
      frequency={5}
      amplitude={25}
      speed={0.5}
      color="#ef4444"
      opacity={0.4}
    />
  );
}