Web InnoventixFreeCode

Caption Slide Right Effect

Original · free

A caption panel slides in from the left on hover.

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

import { useId } from "react";

type Shot = {
  src: string;
  alt: string;
  eyebrow: string;
  title: string;
  copy: string;
};

const SHOTS: Shot[] = [
  {
    src: "/img/gallery/g07.webp",
    alt: "Sunlit ridgeline fading into layered morning haze",
    eyebrow: "Field notes",
    title: "Ridgeline at first light",
    copy: "Shot handheld at 6:14 a.m. before the haze burned off the valley.",
  },
  {
    src: "/img/gallery/g19.webp",
    alt: "Quiet architectural facade with strong diagonal shadows",
    eyebrow: "City study",
    title: "Concrete and shadow",
    copy: "The building only works for about ten minutes of hard afternoon light.",
  },
  {
    src: "/img/gallery/g28.webp",
    alt: "Still water reflecting a muted, low-contrast sky",
    eyebrow: "On location",
    title: "Reflection, held still",
    copy: "No wind, no boats. We waited two hours for the surface to go glass.",
  },
];

export default function ImgfxCaptionSlideRight() {
  const uid = useId().replace(/[^a-zA-Z0-9]/g, "");
  const prefix = `csr${uid}`;

  return (
    <section className="relative w-full bg-neutral-50 py-20 dark:bg-neutral-950 sm:py-28">
      <style>{`
        .${prefix}-panel {
          transform: translateX(-101%);
          transition: transform 520ms cubic-bezier(0.22, 1, 0.36, 1),
                      opacity 300ms ease;
          opacity: 0;
          will-change: transform;
        }
        .${prefix}-card:hover .${prefix}-panel,
        .${prefix}-card:focus-within .${prefix}-panel {
          transform: translateX(0);
          opacity: 1;
        }
        .${prefix}-img {
          transition: transform 620ms cubic-bezier(0.22, 1, 0.36, 1),
                      filter 420ms ease;
        }
        .${prefix}-card:hover .${prefix}-img,
        .${prefix}-card:focus-within .${prefix}-img {
          transform: scale(1.05);
          filter: saturate(1.08);
        }
        .${prefix}-accent {
          transform: scaleY(0);
          transform-origin: top;
          transition: transform 520ms cubic-bezier(0.22, 1, 0.36, 1) 90ms;
        }
        .${prefix}-card:hover .${prefix}-accent,
        .${prefix}-card:focus-within .${prefix}-accent {
          transform: scaleY(1);
        }
        @media (prefers-reduced-motion: reduce) {
          .${prefix}-panel,
          .${prefix}-img,
          .${prefix}-accent {
            transition: opacity 200ms ease;
          }
          .${prefix}-card:hover .${prefix}-panel,
          .${prefix}-card:focus-within .${prefix}-panel,
          .${prefix}-card:hover .${prefix}-img,
          .${prefix}-card:focus-within .${prefix}-img,
          .${prefix}-card:hover .${prefix}-accent,
          .${prefix}-card:focus-within .${prefix}-accent {
            transform: none;
          }
        }
      `}</style>

      <div className="mx-auto max-w-6xl px-6">
        <div className="mb-12 max-w-2xl">
          <p className="text-xs font-semibold uppercase tracking-[0.22em] text-indigo-600 dark:text-indigo-400">
            Image effect
          </p>
          <h2 className="mt-3 text-3xl font-semibold tracking-tight text-neutral-900 dark:text-neutral-50 sm:text-4xl">
            Caption slide-in
          </h2>
          <p className="mt-4 text-base leading-relaxed text-neutral-600 dark:text-neutral-400">
            A caption panel glides in from the left edge, revealing the story
            behind the frame.{" "}
            <span className="font-medium text-neutral-800 dark:text-neutral-200">
              Hover or focus a photo to reveal it.
            </span>
          </p>
        </div>

        <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {SHOTS.map((shot) => (
            <figure
              key={shot.src}
              tabIndex={0}
              className={`${prefix}-card group relative aspect-[4/5] overflow-hidden rounded-2xl bg-neutral-200 shadow-sm outline-none ring-1 ring-neutral-900/5 transition-shadow duration-300 hover:shadow-xl 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={shot.src}
                alt={shot.alt}
                loading="lazy"
                draggable={false}
                className={`${prefix}-img h-full w-full object-cover`}
              />

              {/* readability scrim */}
              <div
                aria-hidden="true"
                className="pointer-events-none absolute inset-0 bg-gradient-to-t from-neutral-950/50 via-neutral-950/5 to-transparent"
              />

              <figcaption className="absolute inset-x-0 bottom-0 p-4">
                <div
                  className={`${prefix}-panel relative overflow-hidden rounded-xl border border-white/60 bg-white/85 p-4 pl-5 shadow-lg backdrop-blur-md dark:border-white/10 dark:bg-neutral-900/85`}
                >
                  <span
                    aria-hidden="true"
                    className={`${prefix}-accent absolute inset-y-2 left-0 w-[3px] rounded-full bg-indigo-500 dark:bg-indigo-400`}
                  />
                  <p className="text-[0.65rem] font-semibold uppercase tracking-[0.18em] text-indigo-600 dark:text-indigo-400">
                    {shot.eyebrow}
                  </p>
                  <p className="mt-1 text-sm font-semibold text-neutral-900 dark:text-neutral-50">
                    {shot.title}
                  </p>
                  <p className="mt-1 text-xs leading-relaxed text-neutral-600 dark:text-neutral-300">
                    {shot.copy}
                  </p>
                </div>
              </figcaption>
            </figure>
          ))}
        </div>
      </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 →