Web InnoventixFreeCode

Hover Blur Focus Gallery

Original · free

A grid where non-hovered images blur and dim while the hovered one stays sharp and lifts.

byWeb InnoventixReact + Tailwind
hoverblurfocusgalleryimages
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/gallery-hover-blur-focus.json
gallery-hover-blur-focus.tsx
"use client";

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

type Shape = "portrait" | "landscape" | "square";

interface Photo {
  src: string;
  alt: string;
  title: string;
  category: string;
  location: string;
  shape: Shape;
}

const PHOTOS: Photo[] = [
  {
    src: "/img/gallery/g03.webp",
    alt: "Frost-covered pine branches at first light in an alpine valley",
    title: "First Light, Selkirks",
    category: "Landscape",
    location: "British Columbia",
    shape: "portrait",
  },
  {
    src: "/img/gallery/g11.webp",
    alt: "Neon signage reflected in a rain-slicked side street at night",
    title: "Wet Neon",
    category: "Street",
    location: "Osaka, Japan",
    shape: "landscape",
  },
  {
    src: "/img/gallery/g07.webp",
    alt: "Close portrait of a weathered fisherman mending a green net",
    title: "The Netmender",
    category: "Portrait",
    location: "Essaouira, Morocco",
    shape: "square",
  },
  {
    src: "/img/gallery/g19.webp",
    alt: "Sweeping sand dune ridgeline under a low golden sun",
    title: "Ridge & Shadow",
    category: "Landscape",
    location: "Sossusvlei, Namibia",
    shape: "landscape",
  },
  {
    src: "/img/gallery/g24.webp",
    alt: "Spiral marble staircase photographed straight down from above",
    title: "Vertigo",
    category: "Architecture",
    location: "Lisbon, Portugal",
    shape: "portrait",
  },
  {
    src: "/img/gallery/g14.webp",
    alt: "Steam rising off espresso cups on a cold market morning",
    title: "Morning Ritual",
    category: "Documentary",
    location: "Turin, Italy",
    shape: "square",
  },
  {
    src: "/img/gallery/g28.webp",
    alt: "Lone surfer paddling out through backlit turquoise water",
    title: "Paddle Out",
    category: "Adventure",
    location: "Nazaré, Portugal",
    shape: "landscape",
  },
  {
    src: "/img/gallery/g31.webp",
    alt: "Detail of hand-dyed indigo textiles hung to dry in the sun",
    title: "Indigo Lines",
    category: "Craft",
    location: "Nishijin, Kyoto",
    shape: "portrait",
  },
  {
    src: "/img/gallery/g05.webp",
    alt: "Snow leopard resting on a rocky outcrop above a river gorge",
    title: "Ghost of the Ranges",
    category: "Wildlife",
    location: "Hemis, Ladakh",
    shape: "square",
  },
  {
    src: "/img/gallery/g22.webp",
    alt: "Bicycle leaning against a mustard-yellow wall in soft afternoon light",
    title: "Yellow Wall",
    category: "Street",
    location: "Cartagena, Colombia",
    shape: "landscape",
  },
];

const SHAPE_SPAN: Record<Shape, string> = {
  portrait: "sm:row-span-2",
  landscape: "sm:col-span-2",
  square: "",
};

const SHAPE_RATIO: Record<Shape, string> = {
  portrait: "aspect-[4/5]",
  landscape: "aspect-[13/9]",
  square: "aspect-square",
};

export default function GalleryHoverBlurFocus() {
  const [active, setActive] = useState<number | null>(null);
  const sectionRef = useRef<HTMLElement>(null);
  const inView = useInView(sectionRef, { once: true, margin: "-80px" });

  const clear = useCallback(() => setActive(null), []);

  return (
    <section
      ref={sectionRef}
      onMouseLeave={clear}
      className="ghbf-scope relative w-full overflow-hidden bg-neutral-50 px-5 py-20 text-neutral-900 sm:px-8 sm:py-28 dark:bg-neutral-950 dark:text-neutral-50"
    >
      <style>{`
        @keyframes ghbf-rise {
          from { opacity: 0; transform: translateY(26px) scale(0.985); }
          to   { opacity: 1; transform: translateY(0) scale(1); }
        }
        @keyframes ghbf-sheen {
          0%   { transform: translateX(-120%) skewX(-12deg); }
          100% { transform: translateX(220%) skewX(-12deg); }
        }
        .ghbf-scope .ghbf-card {
          animation: ghbf-rise 0.7s cubic-bezier(0.22, 1, 0.36, 1) both;
        }
        @media (prefers-reduced-motion: reduce) {
          .ghbf-scope .ghbf-card { animation: none !important; }
          .ghbf-scope .ghbf-sheen { display: none !important; }
          .ghbf-scope [data-blurred="true"] { filter: none !important; }
        }
      `}</style>

      {/* ambient glow */}
      <div
        aria-hidden
        className="pointer-events-none absolute -top-32 left-1/2 h-72 w-[46rem] max-w-[90vw] -translate-x-1/2 rounded-full bg-gradient-to-r from-indigo-400/25 via-violet-400/20 to-rose-300/25 blur-3xl dark:from-indigo-500/20 dark:via-violet-500/15 dark:to-rose-500/15"
      />

      <div className="relative mx-auto max-w-6xl">
        <header className="mb-12 flex flex-col gap-4 sm:mb-16 sm:flex-row sm:items-end sm:justify-between">
          <div>
            <span className="inline-flex items-center gap-2 rounded-full border border-neutral-300 bg-white/70 px-3 py-1 text-[0.7rem] font-medium uppercase tracking-[0.18em] text-neutral-600 backdrop-blur dark:border-neutral-700 dark:bg-neutral-900/70 dark:text-neutral-400">
              <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
              Field Notebook
            </span>
            <h2 className="mt-4 text-3xl font-semibold tracking-tight sm:text-4xl md:text-5xl">
              Ten frames, one gaze
            </h2>
          </div>
          <p className="max-w-sm text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
            Hover any photograph and the rest of the room quiets down. The frame
            you hold stays sharp, lifts toward you, and keeps its color.
          </p>
        </header>

        <div className="grid grid-cols-2 gap-3 sm:auto-rows-[13rem] sm:grid-cols-3 sm:gap-4 lg:grid-cols-4">
          {PHOTOS.map((photo, i) => {
            const isActive = active === i;
            const isBlurred = active !== null && !isActive;

            return (
              <motion.figure
                key={photo.src}
                className={`group relative overflow-hidden rounded-2xl border border-neutral-200/70 bg-neutral-200 shadow-sm dark:border-neutral-800 dark:bg-neutral-900 ${SHAPE_SPAN[photo.shape]}`}
                data-blurred={isBlurred}
                initial={false}
                animate={{
                  scale: isActive ? 1.035 : isBlurred ? 0.975 : 1,
                  y: isActive ? -8 : 0,
                  filter: isBlurred
                    ? "blur(5px) brightness(0.72) saturate(0.85)"
                    : "blur(0px) brightness(1) saturate(1)",
                  zIndex: isActive ? 20 : 1,
                  boxShadow: isActive
                    ? "0 24px 50px -12px rgba(0,0,0,0.35)"
                    : "0 1px 2px rgba(0,0,0,0.06)",
                }}
                transition={{
                  type: "spring",
                  stiffness: 260,
                  damping: 28,
                  filter: { duration: 0.4, ease: "easeOut" },
                }}
                onMouseEnter={() => setActive(i)}
                onFocusCapture={() => setActive(i)}
                onBlurCapture={clear}
              >
                <a
                  href={photo.src}
                  target="_blank"
                  rel="noopener"
                  aria-label={`${photo.title} — ${photo.category}, ${photo.location}. Open full image.`}
                  className={`ghbf-card block h-full w-full rounded-2xl outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-50 dark:focus-visible:ring-offset-neutral-950 ${
                    photo.shape === "square" ? SHAPE_RATIO.square : "h-full"
                  } sm:aspect-auto ${photo.shape !== "square" ? SHAPE_RATIO[photo.shape] : ""} sm:h-full`}
                  style={{ animationDelay: `${(inView ? i : 0) * 70}ms` }}
                >
                  {/* eslint-disable-next-line @next/next/no-img-element */}
                  <img
                    src={photo.src}
                    alt={photo.alt}
                    loading="lazy"
                    draggable={false}
                    className="h-full w-full select-none object-cover"
                  />

                  {/* sheen pass on hover */}
                  <span
                    aria-hidden
                    className="ghbf-sheen pointer-events-none absolute inset-0 overflow-hidden rounded-2xl"
                  >
                    <span className="absolute -inset-y-2 left-0 w-1/3 bg-gradient-to-r from-transparent via-white/25 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 group-hover:[animation:ghbf-sheen_0.9s_ease-in-out]" />
                  </span>

                  {/* caption gradient */}
                  <span
                    aria-hidden
                    className={`pointer-events-none absolute inset-x-0 bottom-0 h-2/3 bg-gradient-to-t from-black/80 via-black/25 to-transparent transition-opacity duration-300 ${
                      isActive ? "opacity-100" : "opacity-0 sm:opacity-70"
                    }`}
                  />

                  <figcaption
                    className={`absolute inset-x-0 bottom-0 flex flex-col gap-1 p-3 transition-all duration-300 sm:p-4 ${
                      isActive
                        ? "translate-y-0 opacity-100"
                        : "translate-y-1 opacity-0 sm:translate-y-2 sm:opacity-90"
                    }`}
                  >
                    <span className="w-fit rounded-full bg-white/15 px-2 py-0.5 text-[0.6rem] font-semibold uppercase tracking-[0.16em] text-white/90 backdrop-blur-sm ring-1 ring-white/20">
                      {photo.category}
                    </span>
                    <span className="text-sm font-semibold leading-tight text-white sm:text-base">
                      {photo.title}
                    </span>
                    <span className="text-[0.7rem] font-medium text-white/70">
                      {photo.location}
                    </span>
                  </figcaption>
                </a>
              </motion.figure>
            );
          })}
        </div>

        <p className="mt-10 text-center text-xs text-neutral-500 dark:text-neutral-500">
          Tab through the frames — focus behaves exactly like hover.
        </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 →
Hover Caption Gallery

Hover Caption Gallery

Original

A responsive image grid where each tile subtly zooms and reveals a caption bar on hover.

Masonry Gallery

Masonry Gallery

Original

A Pinterest-style masonry gallery using CSS columns with varied-height images; tiles lift and brighten on hover.

Filterable Gallery

Filterable Gallery

Original

A filterable gallery with category tabs; clicking a tab animates the grid to show only that category.

Tilt Cards Gallery

Tilt Cards Gallery

Original

A grid of image cards that tilt in 3D toward the cursor on hover, with a soft glare.

Lightbox Gallery

Lightbox Gallery

Original

A thumbnail grid where clicking a thumb opens a full-screen lightbox with prev/next arrows and keyboard support.

Expanding Strip Gallery

Expanding Strip Gallery

Original

A horizontal strip of image panels; hovering a panel expands it and shrinks the others to reveal its caption.

Marquee Gallery

Marquee Gallery

Original

Two rows of images auto-scrolling in opposite directions, pausing on hover, with edge fade masks.

Hover Zoom Overlay Gallery

Hover Zoom Overlay Gallery

Original

A grid where each image scales up under a dark gradient overlay revealing a title and a view button on hover.

Polaroid Stack Gallery

Polaroid Stack Gallery

Original

A scattered stack of slightly-rotated polaroid photos that straighten and lift on hover.

Featured Thumbs Gallery

Featured Thumbs Gallery

Original

A large featured image with a thumbnail row; selecting a thumbnail swaps the featured image with a crossfade.

Carousel Gallery

Carousel Gallery

Original

An image carousel with arrows and dots, smooth slide transitions and autoplay that pauses on hover.

Duotone Hover Gallery

Duotone Hover Gallery

Original

A grid where images sit under a coloured duotone wash that clears to full colour on hover.