Web InnoventixFreeCode

Caption Slide Up Effect

Original · free

A caption panel slides up from the bottom on hover.

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

import { useId } from "react";

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

const SLIDES: Slide[] = [
  {
    src: "/img/gallery/g07.webp",
    alt: "Golden light spilling across a quiet mountain ridge at dawn",
    kicker: "Field notes",
    title: "First light, high ridge",
    caption:
      "Shot forty minutes before sunrise, when the valley still holds the cold.",
  },
  {
    src: "/img/gallery/g19.webp",
    alt: "Still water mirroring a soft evening sky",
    kicker: "Field notes",
    title: "The mirror hour",
    caption: "No wind, no ripple — just the sky borrowing the lake for a while.",
  },
  {
    src: "/img/gallery/g28.webp",
    alt: "Detail of textured stone catching low directional light",
    kicker: "Field notes",
    title: "Grain & shadow",
    caption: "A close read of stone, lit so every ridge earns its own edge.",
  },
];

export default function ImgfxCaptionSlideUp() {
  const styleId = useId().replace(/[:]/g, "");
  const prefix = `csu-${styleId}`;

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

      <div className="mx-auto max-w-6xl">
        <header className="mx-auto mb-12 max-w-2xl text-center">
          <p className="text-xs font-semibold uppercase tracking-[0.25em] 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-up
          </h2>
          <p className="mt-4 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
            A caption panel glides up from the bottom edge on hover. Hover or
            focus a photo to read the story behind it.
          </p>
        </header>

        <ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {SLIDES.map((slide) => (
            <li key={slide.src}>
              <article
                tabIndex={0}
                aria-label={`${slide.title} — ${slide.caption}`}
                className={`${prefix}-card group relative block aspect-[4/5] w-full overflow-hidden rounded-2xl bg-neutral-200 shadow-sm outline-none ring-1 ring-neutral-900/10 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={slide.src}
                  alt={slide.alt}
                  loading="lazy"
                  draggable={false}
                  className={`${prefix}-img absolute inset-0 h-full w-full object-cover`}
                />

                <span
                  aria-hidden="true"
                  className="pointer-events-none absolute right-4 top-4 rounded-full bg-white/85 px-3 py-1 text-[0.65rem] font-semibold uppercase tracking-[0.18em] text-neutral-700 backdrop-blur-sm transition-opacity duration-300 group-hover:opacity-0 group-focus-within:opacity-0 dark:bg-neutral-900/80 dark:text-neutral-200">
                  Hover
                </span>

                <div
                  className={`${prefix}-panel absolute inset-x-0 bottom-0`}
                >
                  <div className="bg-gradient-to-t from-neutral-950 via-neutral-950/95 to-neutral-950/70 px-5 pb-5 pt-8 text-left">
                    <p className="text-[0.65rem] font-semibold uppercase tracking-[0.2em] text-indigo-300">
                      {slide.kicker}
                    </p>
                    <h3 className="mt-1 text-lg font-semibold text-white">
                      {slide.title}
                    </h3>
                    <p
                      className={`${prefix}-line mt-1.5 text-sm leading-relaxed text-neutral-300`}
                    >
                      {slide.caption}
                    </p>
                  </div>
                </div>
              </article>
            </li>
          ))}
        </ul>
      </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 →