Web InnoventixFreeCode

Caption Slide Left Effect

Original · free

A caption panel slides in from the right on hover.

byWeb InnoventixReact + Tailwind
captionslideleftimageeffect
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-caption-slide-left.json
imgfx-caption-slide-left.tsx
"use client";

import { useState } from "react";

type Slide = {
  src: string;
  alt: string;
  kicker: string;
  title: string;
  body: string;
};

const SLIDES: Slide[] = [
  {
    src: "/img/gallery/g07.webp",
    alt: "Sunlight breaking over a ridge of layered mountains",
    kicker: "Field notes",
    title: "First light, last ridge",
    body: "Forty minutes before the trailhead crowd arrives, the valley holds its breath and the color does all the talking.",
  },
  {
    src: "/img/gallery/g14.webp",
    alt: "Quiet city street reflecting neon after rain",
    kicker: "After hours",
    title: "The wet-pavement hour",
    body: "Rain turns every gutter into a mirror. Shoot low, let the reflections carry the frame, and the street doubles itself.",
  },
  {
    src: "/img/gallery/g22.webp",
    alt: "Close detail of textured desert dunes at dusk",
    kicker: "Texture study",
    title: "Where the wind writes",
    body: "Side light at dusk rakes across the sand and every ripple earns a shadow. Nothing here stays still for long.",
  },
];

export default function ImgfxCaptionSlideLeft() {
  const [active, setActive] = useState<number | null>(null);

  return (
    <section className="relative w-full bg-neutral-50 px-6 py-20 dark:bg-neutral-950 sm:px-10 sm:py-28">
      <style>{`
        @media (prefers-reduced-motion: reduce) {
          .csl-panel, .csl-media img {
            transition: none !important;
          }
        }
      `}</style>

      <div className="mx-auto max-w-6xl">
        <header className="mb-12 text-center">
          <p className="text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
            Image effect
          </p>
          <h2 className="mt-3 text-3xl font-bold tracking-tight text-neutral-900 dark:text-neutral-50 sm:text-4xl">
            Caption Slide-In
          </h2>
          <p className="mx-auto mt-4 max-w-xl text-sm text-neutral-600 dark:text-neutral-400">
            A caption panel glides in from the right and settles over the frame. Hover a photo, or focus it with the keyboard, to reveal the story.
          </p>
        </header>

        <ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {SLIDES.map((slide, i) => {
            const open = active === i;
            return (
              <li key={slide.src}>
                <figure
                  tabIndex={0}
                  onMouseEnter={() => setActive(i)}
                  onMouseLeave={() => setActive((prev) => (prev === i ? null : prev))}
                  onFocus={() => setActive(i)}
                  onBlur={() => setActive((prev) => (prev === i ? null : prev))}
                  className="group relative aspect-[3/4] w-full overflow-hidden rounded-2xl bg-neutral-200 shadow-sm ring-1 ring-black/5 outline-none transition-shadow duration-300 focus-visible:ring-2 focus-visible:ring-indigo-500 hover:shadow-xl dark:bg-neutral-800 dark:ring-white/10"
                >
                  <div className="csl-media absolute inset-0">
                    {/* eslint-disable-next-line @next/next/no-img-element */}
                    <img
                      src={slide.src}
                      alt={slide.alt}
                      loading="lazy"
                      draggable={false}
                      className="h-full w-full object-cover transition-transform duration-[700ms] ease-out group-hover:scale-105 group-focus-visible:scale-105"
                    />
                  </div>

                  <div
                    aria-hidden="true"
                    className={`pointer-events-none absolute inset-0 bg-gradient-to-l from-black/50 via-black/10 to-transparent transition-opacity duration-500 ${
                      open ? "opacity-100" : "opacity-0"
                    }`}
                  />

                  <figcaption
                    className={`csl-panel absolute inset-y-0 right-0 flex w-[82%] max-w-[19rem] flex-col justify-end gap-2 bg-white/95 p-6 shadow-[-8px_0_24px_-12px_rgba(0,0,0,0.45)] backdrop-blur-sm transition-transform duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] dark:bg-neutral-900/95 ${
                      open ? "translate-x-0" : "translate-x-full"
                    }`}
                  >
                    <p className="text-[0.65rem] font-semibold uppercase tracking-[0.22em] text-indigo-600 dark:text-indigo-400">
                      {slide.kicker}
                    </p>
                    <h3 className="text-lg font-bold leading-snug text-neutral-900 dark:text-neutral-50">
                      {slide.title}
                    </h3>
                    <p className="text-sm leading-relaxed text-neutral-600 dark:text-neutral-300">
                      {slide.body}
                    </p>
                    <span className="mt-1 h-0.5 w-10 rounded-full bg-indigo-500/80" />
                  </figcaption>
                </figure>
              </li>
            );
          })}
        </ul>

        <p className="mt-10 text-center text-xs font-medium uppercase tracking-[0.18em] text-neutral-400 dark:text-neutral-500">
          Hover or focus to reveal
        </p>
      </div>
    </section>
  );
}

Dependencies

None (React + Tailwind only).

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 →