Web InnoventixFreeCode

Wave Distort Effect

Original · free

A gentle wave distortion ripples across the image on hover.

byWeb InnoventixReact + Tailwind
wavedistortimageeffect
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/imgfx-wave-distort.json
imgfx-wave-distort.tsx
"use client";

import { useRef } from "react";
import { motion, useInView, useReducedMotion } from "motion/react";

export default function ImgfxWaveDistort() {
  const sectionRef = useRef<HTMLDivElement>(null);
  const inView = useInView(sectionRef, { once: true, margin: "-80px" });
  const reduce = useReducedMotion();

  return (
    <section className="relative w-full overflow-hidden bg-neutral-50 px-6 py-20 sm:px-10 sm:py-28 dark:bg-neutral-950">
      <style>{`
        @keyframes imgfxWave_lift {
          0%   { transform: translateY(10px); opacity: 0; }
          100% { transform: translateY(0);    opacity: 1; }
        }
        @keyframes imgfxWave_hint {
          0%, 100% { transform: translateX(0); }
          50%      { transform: translateX(4px); }
        }
        @media (prefers-reduced-motion: reduce) {
          .imgfxWave_liftEl { animation: none !important; opacity: 1 !important; transform: none !important; }
          .imgfxWave_hintEl { animation: none !important; }
          .imgfxWave_ripple { opacity: 0 !important; }
        }
      `}</style>

      {/* Hidden SVG carrying the animated displacement filter */}
      <svg
        aria-hidden="true"
        className="pointer-events-none absolute h-0 w-0"
        focusable="false"
      >
        <defs>
          <filter
            id="imgfxWaveDistortFilter"
            x="-20%"
            y="-20%"
            width="140%"
            height="140%"
          >
            <feTurbulence
              type="fractalNoise"
              baseFrequency="0.008 0.013"
              numOctaves="2"
              seed="7"
              result="imgfxNoise"
            >
              <animate
                attributeName="baseFrequency"
                dur="14s"
                values="0.006 0.010; 0.011 0.018; 0.006 0.010"
                repeatCount="indefinite"
                calcMode="spline"
                keyTimes="0; 0.5; 1"
                keySplines="0.4 0 0.6 1; 0.4 0 0.6 1"
              />
            </feTurbulence>
            <feDisplacementMap
              in="SourceGraphic"
              in2="imgfxNoise"
              scale="20"
              xChannelSelector="R"
              yChannelSelector="G"
            />
          </filter>
        </defs>
      </svg>

      <div ref={sectionRef} className="mx-auto max-w-2xl">
        <motion.div
          initial={reduce ? false : { opacity: 0, y: 16 }}
          animate={inView && !reduce ? { opacity: 1, y: 0 } : undefined}
          transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
        >
          <p className="text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
            Wave Distort
          </p>
          <h2 className="mt-3 text-2xl font-semibold tracking-tight text-neutral-900 sm:text-3xl dark:text-neutral-50">
            Water it, and it moves
          </h2>

          {/* Image with ripple-on-hover */}
          <div className="group imgfxWave_liftEl mt-8 [animation:imgfxWave_lift_0.7s_cubic-bezier(0.22,1,0.36,1)_both]">
            <figure className="relative overflow-hidden rounded-2xl ring-1 ring-neutral-900/10 dark:ring-white/10">
              {/* Clean base layer */}
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                src="/img/gallery/g14.webp"
                alt="Sunlit coastal cliffs meeting a calm turquoise sea"
                loading="lazy"
                draggable={false}
                className="block aspect-[4/3] w-full object-cover"
              />

              {/* Rippling overlay layer — fades in and distorts on hover */}
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                src="/img/gallery/g14.webp"
                alt=""
                aria-hidden="true"
                loading="lazy"
                draggable={false}
                className="imgfxWave_ripple absolute inset-0 block h-full w-full object-cover opacity-0 transition-opacity duration-500 ease-out group-hover:opacity-100 group-focus-within:opacity-100"
                style={{ filter: "url(#imgfxWaveDistortFilter)" }}
              />

              {/* Sheen that follows the reveal */}
              <div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-neutral-950/20 to-transparent opacity-0 transition-opacity duration-500 group-hover:opacity-100" />
            </figure>
          </div>

          <p className="mt-4 flex items-center gap-1.5 text-sm text-neutral-500 dark:text-neutral-400">
            Hover the photo to send a slow ripple across the surface
            <span
              className="imgfxWave_hintEl inline-block [animation:imgfxWave_hint_1.6s_ease-in-out_infinite]"
              aria-hidden="true"
            >
              &rarr;
            </span>
          </p>
        </motion.div>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →