Web InnoventixFreeCode

Focus Highlight Effect

Original · free

Hovering highlights the image and dims a caption row.

byWeb InnoventixReact + Tailwind
focushighlightimageeffect
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-focus-highlight.json
imgfx-focus-highlight.tsx
"use client";

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

type Shot = {
  src: string;
  alt: string;
  title: string;
  meta: string;
};

const SHOTS: Shot[] = [
  {
    src: "/img/gallery/g07.webp",
    alt: "Sunlit ridline above a valley of low cloud at first light",
    title: "First Light, Alta Ridge",
    meta: "35mm · f/8 · golden hour",
  },
  {
    src: "/img/gallery/g14.webp",
    alt: "Rain-slick city street reflecting neon storefronts after dusk",
    title: "Wet Neon, 9th & Main",
    meta: "50mm · f/1.8 · long exposure",
  },
  {
    src: "/img/gallery/g22.webp",
    alt: "Close portrait of a potter's hands shaping wet clay on a wheel",
    title: "Hands at the Wheel",
    meta: "85mm · f/2.0 · window light",
  },
  {
    src: "/img/gallery/g31.webp",
    alt: "Wind-carved dunes throwing long shadows across pale desert sand",
    title: "Dune Field, Late Afternoon",
    meta: "24mm · f/11 · side light",
  },
];

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

  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 ifh_rise {
          from { opacity: 0; transform: translateY(22px); }
          to { opacity: 1; transform: translateY(0); }
        }
        @keyframes ifh_sheen {
          0% { transform: translateX(-120%) skewX(-12deg); }
          100% { transform: translateX(220%) skewX(-12deg); }
        }
        .ifh-card { position: relative; }
        .ifh-frame {
          position: relative;
          overflow: hidden;
          border-radius: 1rem;
          transition: transform .5s cubic-bezier(.16,1,.3,1),
                      box-shadow .5s cubic-bezier(.16,1,.3,1);
          will-change: transform;
        }
        .ifh-frame img {
          transition: transform .7s cubic-bezier(.16,1,.3,1),
                      filter .5s ease;
          filter: saturate(.8) brightness(.92) contrast(.98);
          will-change: transform, filter;
        }
        .ifh-veil {
          position: absolute; inset: 0;
          background: radial-gradient(120% 80% at 50% 20%, transparent 40%, rgba(10,10,12,.28) 100%);
          opacity: 1;
          transition: opacity .5s ease;
          pointer-events: none;
        }
        .ifh-sheen {
          position: absolute; top: 0; bottom: 0; left: 0;
          width: 45%;
          background: linear-gradient(100deg, transparent, rgba(255,255,255,.35), transparent);
          transform: translateX(-120%) skewX(-12deg);
          opacity: 0;
          pointer-events: none;
        }
        .ifh-caption {
          transition: opacity .45s ease, transform .45s ease, filter .45s ease;
        }

        /* Hover state: highlight the image, dim the caption row */
        .ifh-card:hover .ifh-frame {
          transform: translateY(-6px) scale(1.012);
          box-shadow: 0 24px 60px -20px rgba(79,70,229,.45), 0 8px 24px -12px rgba(0,0,0,.35);
        }
        .ifh-card:hover .ifh-frame img {
          transform: scale(1.06);
          filter: saturate(1.12) brightness(1.06) contrast(1.04);
        }
        .ifh-card:hover .ifh-veil { opacity: 0; }
        .ifh-card:hover .ifh-sheen { animation: ifh_sheen 1.1s cubic-bezier(.16,1,.3,1) forwards; opacity: 1; }
        .ifh-card:hover .ifh-caption { opacity: .35; transform: translateY(2px); filter: blur(.3px); }
        .ifh-card:focus-within .ifh-frame {
          transform: translateY(-6px) scale(1.012);
          box-shadow: 0 24px 60px -20px rgba(79,70,229,.45), 0 8px 24px -12px rgba(0,0,0,.35);
        }
        .ifh-card:focus-within .ifh-frame img {
          transform: scale(1.06);
          filter: saturate(1.12) brightness(1.06) contrast(1.04);
        }
        .ifh-card:focus-within .ifh-veil { opacity: 0; }
        .ifh-card:focus-within .ifh-caption { opacity: .35; }

        @media (prefers-reduced-motion: reduce) {
          .ifh-frame, .ifh-frame img, .ifh-caption, .ifh-veil { transition: none; }
          .ifh-card:hover .ifh-frame,
          .ifh-card:focus-within .ifh-frame { transform: none; }
          .ifh-card:hover .ifh-frame img,
          .ifh-card:focus-within .ifh-frame img { transform: none; }
          .ifh-card:hover .ifh-sheen,
          .ifh-card:focus-within .ifh-sheen { animation: none; opacity: 0; }
        }
      `}</style>

      <div ref={sectionRef} className="mx-auto max-w-6xl">
        <div className="mb-12 max-w-2xl">
          <span className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.14em] text-indigo-700 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300">
            <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
            Focus Highlight
          </span>
          <h2 className="mt-5 text-3xl font-semibold tracking-tight text-neutral-900 sm:text-4xl dark:text-neutral-50">
            Look, and the frame answers.
          </h2>
          <p className="mt-3 text-base text-neutral-600 dark:text-neutral-400">
            Hover a photo to bring it into full colour and light while its caption steps quietly back.
          </p>
        </div>

        <div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
          {SHOTS.map((shot, i) => (
            <motion.figure
              key={shot.src}
              className="ifh-card m-0"
              initial={reduce ? false : { opacity: 0, y: 22 }}
              animate={
                reduce
                  ? undefined
                  : inView
                    ? { opacity: 1, y: 0 }
                    : { opacity: 0, y: 22 }
              }
              transition={{ duration: 0.55, delay: i * 0.09, ease: [0.16, 1, 0.3, 1] }}
            >
              <a
                href="#"
                onClick={(e) => e.preventDefault()}
                className="ifh-frame block rounded-2xl outline-none ring-0 ring-indigo-500 focus-visible:ring-2"
                aria-label={`View ${shot.title}`}
              >
                {/* eslint-disable-next-line @next/next/no-img-element */}
                <img
                  src={shot.src}
                  alt={shot.alt}
                  loading="lazy"
                  draggable={false}
                  className="aspect-[4/3] w-full object-cover"
                />
                <span className="ifh-veil" aria-hidden="true" />
                <span className="ifh-sheen" aria-hidden="true" />
              </a>

              <figcaption className="ifh-caption mt-4 flex items-baseline justify-between gap-4">
                <span className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
                  {shot.title}
                </span>
                <span className="shrink-0 text-xs font-mono uppercase tracking-wide text-neutral-500 dark:text-neutral-400">
                  {shot.meta}
                </span>
              </figcaption>
            </motion.figure>
          ))}
        </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 →