Wavy Tide Background

Animated canvas waves that fill the container and drift with the pointer — a softer, SaaS-ready take on classic wavy backgrounds.

Add to your project

Pick the way you like to build.

Let your AI assistant add it

Copy this prompt and paste it into the AI tool building your site.

# Integrate this UI component

Add this component to my project.

1. Install any missing dependencies.
2. Make sure Tailwind CSS is set up and scans the new file.
3. Prefer passing props over editing the component source — changing the component itself may break it.
4. Adjust colors, size, and layout via props or wrapper classes to match my project. You know my codebase better than this snippet does.

## Component source

```tsx
'use client';

import React, { useEffect, useRef, type ReactNode } from 'react';

// ---------------------------------------------------------------------------
// Props
// ---------------------------------------------------------------------------
//
// children           Content layered above the canvas (headline, CTA, etc.)
// className          Styles for the content wrapper
// containerClassName Styles for the outer shell (size, layout)
// colors             Wave layer colors (hex array); falls back to a warm palette
// backgroundFill     Solid color behind the waves (hex)
// waveWidth          Stroke width of each wave line
// speed              "slow" or "fast" (default) — how quickly waves drift
// waveOpacity        Overall opacity of the wave layers (0–1)
//
type WavyTideBackgroundProps = {
  children?: ReactNode;
  className?: string;
  containerClassName?: string;
  colors?: string[];
  backgroundFill?: string;
  waveWidth?: number;
  speed?: 'slow' | 'fast';
  waveOpacity?: number;
};

// ---------------------------------------------------------------------------
// Wave shape — layered sines + pointer pull for a soft tidal feel
// ---------------------------------------------------------------------------
function waveOffset(
  position: number,
  layer: number,
  t: number,
  pointerX: number,
  pointerY: number
) {
  const pull = (pointerX - 0.5) * 36;
  const lift = (pointerY - 0.5) * 44;
  return (
    Math.sin(position * 0.0035 + t + layer * 0.9) * 38 +
    Math.sin(position * 0.008 - t * 1.4 + layer * 1.7) * 22 +
    Math.sin(position * 0.018 + t * 0.6 + layer) * 10 +
    Math.sin((position + pull * 8) * 0.002 + t * 0.4) * 16 +
    lift * (0.3 + layer * 0.07)
  );
}

export default function WavyTideBackground({
  children,
  className = '',
  containerClassName = '',
  colors,
  backgroundFill = '#140818',
  waveWidth = 2,
  speed = 'fast',
  waveOpacity = 0.85,
}: WavyTideBackgroundProps) {
  // ---------------------------------------------------------------------------
  // Refs
  // ---------------------------------------------------------------------------
  const containerRef = useRef<HTMLDivElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const pointerRef = useRef({ x: 0.5, y: 0.5 });
  const rafRef = useRef(0);
  const timeRef = useRef(0);

  useEffect(() => {
    const canvas = canvasRef.current;
    const container = containerRef.current;
    if (!canvas || !container) return;

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const waveColors = colors ?? [
      '#f97316',
      '#fb7185',
      '#e879f9',
      '#c084fc',
      '#fbbf24',
    ];

    const step = speed === 'fast' ? 0.012 : 0.006;
    const angle = -0.42;
    let width = 0;
    let height = 0;
    let dpr = 1;
    const prefersReducedMotion = window.matchMedia(
      '(prefers-reduced-motion: reduce)'
    ).matches;

    // -------------------------------------------------------------------------
    // Setup helpers
    // -------------------------------------------------------------------------
    const resize = () => {
      const rect = container.getBoundingClientRect();
      dpr = Math.min(window.devicePixelRatio || 1, 2);
      width = Math.max(1, Math.floor(rect.width));
      height = Math.max(1, Math.floor(rect.height));
      canvas.width = Math.floor(width * dpr);
      canvas.height = Math.floor(height * dpr);
      canvas.style.width = `${width}px`;
      canvas.style.height = `${height}px`;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };

    // -------------------------------------------------------------------------
    // Draw loop — fill background, rotate canvas, stroke + fill each wave layer
    // -------------------------------------------------------------------------
    const draw = () => {
      timeRef.current += prefersReducedMotion ? 0 : step;
      const t = timeRef.current;
      const { x: px, y: py } = pointerRef.current;
      const span = Math.hypot(width, height) * 1.35;

      ctx.globalAlpha = 1;
      ctx.fillStyle = backgroundFill;
      ctx.fillRect(0, 0, width, height);

      ctx.save();
      ctx.translate(width / 2, height / 2);
      ctx.rotate(angle);
      ctx.translate(-span / 2, -span / 2);
      ctx.globalAlpha = waveOpacity;

      for (let layer = 0; layer < 5; layer++) {
        const baseline = span * (0.34 + layer * 0.09);
        const color = waveColors[layer % waveColors.length];

        ctx.beginPath();
        ctx.lineWidth = waveWidth;
        ctx.strokeStyle = color;
        ctx.lineCap = 'round';
        ctx.lineJoin = 'round';

        for (let x = 0; x <= span; x += 4) {
          const y = baseline + waveOffset(x, layer, t, px, py);
          if (x === 0) ctx.moveTo(x, y);
          else ctx.lineTo(x, y);
        }
        ctx.stroke();

        // Fill below the wave with a soft fade
        ctx.lineTo(span, span);
        ctx.lineTo(0, span);
        ctx.closePath();

        const gradient = ctx.createLinearGradient(0, baseline - 60, 0, span);
        gradient.addColorStop(0, `${color}40`);
        gradient.addColorStop(1, 'transparent');
        ctx.fillStyle = gradient;
        ctx.fill();
      }

      ctx.restore();

      if (!prefersReducedMotion) {
        rafRef.current = requestAnimationFrame(draw);
      }
    };

    // Pointer position (0–1) nudges the wave offset
    const onPointerMove = (event: PointerEvent) => {
      const rect = container.getBoundingClientRect();
      pointerRef.current = {
        x: (event.clientX - rect.left) / Math.max(rect.width, 1),
        y: (event.clientY - rect.top) / Math.max(rect.height, 1),
      };
    };

    // -------------------------------------------------------------------------
    // Start + cleanup
    // -------------------------------------------------------------------------
    resize();
    draw();

    const observer = new ResizeObserver(resize);
    observer.observe(container);
    container.addEventListener('pointermove', onPointerMove);

    return () => {
      cancelAnimationFrame(rafRef.current);
      observer.disconnect();
      container.removeEventListener('pointermove', onPointerMove);
    };
  }, [backgroundFill, colors, speed, waveOpacity, waveWidth]);

  // ---------------------------------------------------------------------------
  // Render — canvas behind, content on top
  // ---------------------------------------------------------------------------
  return (
    <div
      ref={containerRef}
      className={[
        'relative flex min-h-[420px] w-full flex-col items-center justify-center overflow-hidden',
        containerClassName,
      ]
        .filter(Boolean)
        .join(' ')}
    >
      <canvas
        ref={canvasRef}
        className='absolute inset-0 z-0 h-full w-full'
        aria-hidden
      />
      <div
        className={['relative z-10 px-6 text-center', className]
          .filter(Boolean)
          .join(' ')}
      >
        {children}
      </div>
    </div>
  );
}

```

Works with your AI tools

ClaudeClaude*
CodexCodex*
LovableLovable
Base44Base44
Figma MakeFigma Make
ReplitReplit*
CursorCursor*
v0v0

* Compatible with most projects that use the default React/Next.js framework.

Built With

ReactTailwind CSSCanvas API

Props

PropTypeDescription
childrenReactNodeContent layered above the canvas (headline, CTA, etc.)
classNamestringStyles for the content wrapper
containerClassNamestringStyles for the outer shell (size, layout)
colorsstring[]Wave layer colors (hex); falls back to a warm palette
backgroundFillstringSolid color behind the waves (hex)
waveWidthnumberStroke width of each wave line
speed'slow' | 'fast'How quickly the waves drift
waveOpacitynumberOverall opacity of the wave layers (0–1)

More Backgrounds Components

Subtle canvas meteors drifting across a dark sky — a quiet animated background for heroes and landing sections.

Staggered neon trails that sweep across a dark field — colors are prop-driven for easy theming.

Soft layered waves in parallax drift — cool indigo tones by default, colors via props.

Aurora Background preview

Northern lights as folding canvas curtains: irregular ribbons and faint rays drifting across a night sky.

A static 3D background grid — a wireframe perspective grid that covers the full container, warped and bowed toward a vanishing point for depth. No animation, no layout shift.