Web InnoventixFreeCode

Compare Fade Effect

Original · free

Hover to crossfade between two versions of the image.

byWeb InnoventixReact + Tailwind
comparefadeimageeffect
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-compare-fade.json
imgfx-compare-fade.tsx
"use client";

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

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

  const cards: { src: string; alt: string; label: string }[] = [
    {
      src: "/img/gallery/g07.webp",
      alt: "Morning street scene shown as a warm color photograph",
      label: "Warm grade",
    },
    {
      src: "/img/gallery/g18.webp",
      alt: "Quiet coastline shown as a warm color photograph",
      label: "Golden hour",
    },
  ];

  return (
    <section className="relative w-full overflow-hidden bg-neutral-50 px-6 py-20 sm:py-28 dark:bg-neutral-950">
      <style>{`
        .cmpfade-figure { --cmpfade-t: 620ms; }
        .cmpfade-layer {
          transition: opacity var(--cmpfade-t) cubic-bezier(0.4, 0, 0.2, 1),
                      transform var(--cmpfade-t) cubic-bezier(0.4, 0, 0.2, 1),
                      filter var(--cmpfade-t) cubic-bezier(0.4, 0, 0.2, 1);
          will-change: opacity, transform, filter;
        }
        .cmpfade-alt { opacity: 0; transform: scale(1.06); filter: saturate(1.35) contrast(1.08); }
        .cmpfade-base { opacity: 1; transform: scale(1); filter: saturate(0.55) contrast(0.95) brightness(1.02); }
        .cmpfade-figure:hover .cmpfade-alt,
        .cmpfade-figure:focus-visible .cmpfade-alt { opacity: 1; transform: scale(1); filter: saturate(1.35) contrast(1.08); }
        .cmpfade-figure:hover .cmpfade-base,
        .cmpfade-figure:focus-visible .cmpfade-base { opacity: 0; transform: scale(1.06); }
        .cmpfade-pill { transition: opacity 320ms ease, transform 320ms ease; }
        .cmpfade-pill-a { opacity: 1; }
        .cmpfade-pill-b { opacity: 0; transform: translateY(4px); }
        .cmpfade-figure:hover .cmpfade-pill-a,
        .cmpfade-figure:focus-visible .cmpfade-pill-a { opacity: 0; transform: translateY(-4px); }
        .cmpfade-figure:hover .cmpfade-pill-b,
        .cmpfade-figure:focus-visible .cmpfade-pill-b { opacity: 1; transform: translateY(0); }
        @media (prefers-reduced-motion: reduce) {
          .cmpfade-layer, .cmpfade-pill { transition: opacity 200ms linear; transform: none !important; }
          .cmpfade-alt { transform: none; }
        }
      `}</style>

      <div className="mx-auto max-w-5xl">
        <motion.div
          initial={reduce ? false : { opacity: 0, y: 16 }}
          animate={inView && !reduce ? { opacity: 1, y: 0 } : undefined}
          transition={{ duration: 0.5, ease: [0.4, 0, 0.2, 1] }}
          className="mb-10 text-center"
        >
          <span className="inline-block rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-medium uppercase tracking-widest text-indigo-600 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300">
            Image Effect
          </span>
          <h2 className="mt-4 text-3xl font-semibold tracking-tight text-neutral-900 sm:text-4xl dark:text-neutral-50">
            Crossfade Compare
          </h2>
          <p className="mx-auto mt-3 max-w-md text-sm text-neutral-500 dark:text-neutral-400">
            Hover a frame to dissolve between the muted proof and the finished color grade.
          </p>
        </motion.div>

        <div ref={ref} className="grid gap-6 sm:grid-cols-2">
          {cards.map((card, i) => (
            <motion.figure
              key={card.src}
              initial={reduce ? false : { opacity: 0, y: 24 }}
              animate={inView && !reduce ? { opacity: 1, y: 0 } : undefined}
              transition={{ duration: 0.5, ease: [0.4, 0, 0.2, 1], delay: 0.08 * i }}
              tabIndex={0}
              className="cmpfade-figure group relative aspect-[4/5] overflow-hidden rounded-2xl bg-neutral-200 shadow-sm ring-1 ring-neutral-900/5 outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:bg-neutral-800 dark:ring-white/10"
            >
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                src={card.src}
                alt={`${card.alt} — muted proof version`}
                loading="lazy"
                draggable={false}
                className="cmpfade-layer cmpfade-base absolute inset-0 h-full w-full object-cover"
              />
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                src={card.src}
                alt={`${card.alt} — finished color grade`}
                loading="lazy"
                draggable={false}
                className="cmpfade-layer cmpfade-alt absolute inset-0 h-full w-full object-cover"
              />

              <div className="pointer-events-none absolute inset-x-0 bottom-0 h-2/5 bg-gradient-to-t from-black/60 to-transparent" />

              <figcaption className="pointer-events-none absolute bottom-4 left-4 flex items-center gap-2">
                <span className="relative inline-flex h-6 items-center">
                  <span className="cmpfade-pill cmpfade-pill-a absolute rounded-full bg-white/85 px-2.5 py-1 text-xs font-medium text-neutral-800 backdrop-blur">
                    Proof
                  </span>
                  <span className="cmpfade-pill cmpfade-pill-b absolute rounded-full bg-indigo-500 px-2.5 py-1 text-xs font-medium text-white">
                    {card.label}
                  </span>
                </span>
              </figcaption>
            </motion.figure>
          ))}
        </div>

        <p className="mt-8 text-center text-xs text-neutral-400 dark:text-neutral-500">
          Tip: keyboard users can tab to a frame to trigger the same crossfade.
        </p>
      </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 →