Web InnoventixFreeCode

Flip V Effect

Original · free

Image flips vertically to a caption back on hover.

byWeb InnoventixReact + Tailwind
flipvimageeffect
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-flip-v.json
imgfx-flip-v.tsx
"use client";

import { useId } from "react";

type FlipCard = {
  src: string;
  alt: string;
  title: string;
  caption: string;
  meta: string;
};

const CARDS: FlipCard[] = [
  {
    src: "/img/gallery/g07.webp",
    alt: "Low sun raking across a ridge of wind-carved dunes",
    title: "Dune Light",
    caption:
      "Shot an hour before dusk, when the ridgelines turn to knife edges and the sand holds the last warm colour of the day.",
    meta: "35mm · f/8 · 1/500s",
  },
  {
    src: "/img/gallery/g19.webp",
    alt: "Still mountain lake mirroring a dark treeline",
    title: "Still Water",
    caption:
      "No wind, no ripple. The lake held the whole valley upside down long enough for one clean, patient frame.",
    meta: "50mm · f/11 · 1/125s",
  },
  {
    src: "/img/gallery/g28.webp",
    alt: "City street glowing with neon reflections after rain",
    title: "After Rain",
    caption:
      "Wet asphalt does the work of a second lens, doubling every sign and streetlight into the puddles below.",
    meta: "24mm · f/2.8 · 1/60s",
  },
];

export default function ImgfxFlipV() {
  const rawId = useId();
  const styleId = `imgfxflipv-${rawId.replace(/[^a-zA-Z0-9]/g, "")}`;

  return (
    <section
      className="relative w-full bg-neutral-50 px-6 py-20 dark:bg-neutral-950 sm:py-28"
      aria-labelledby={`${styleId}-heading`}
    >
      <style
        // eslint-disable-next-line react/no-danger
        dangerouslySetInnerHTML={{
          __html: `
            .${styleId}-scene { perspective: 1400px; }
            .${styleId}-card {
              transform-style: preserve-3d;
              transition: transform 700ms cubic-bezier(0.16, 1, 0.3, 1);
              will-change: transform;
            }
            .${styleId}-scene:hover .${styleId}-card,
            .${styleId}-scene:focus-within .${styleId}-card {
              transform: rotateX(180deg);
            }
            .${styleId}-face {
              backface-visibility: hidden;
              -webkit-backface-visibility: hidden;
            }
            .${styleId}-back { transform: rotateX(180deg); }
            @media (prefers-reduced-motion: reduce) {
              .${styleId}-card { transition-duration: 1ms; }
            }
          `,
        }}
      />

      <div className="mx-auto max-w-5xl">
        <div className="mb-12 text-center">
          <p className="text-xs font-semibold uppercase tracking-[0.25em] text-indigo-600 dark:text-indigo-400">
            Image FX · Vertical Flip
          </p>
          <h2
            id={`${styleId}-heading`}
            className="mt-3 text-3xl font-bold tracking-tight text-neutral-900 dark:text-neutral-50 sm:text-4xl"
          >
            Flip to the story
          </h2>
          <p className="mx-auto mt-3 max-w-md text-sm text-neutral-500 dark:text-neutral-400">
            Hover or focus a frame — it turns on its horizontal axis to reveal
            the caption behind the photo.
          </p>
        </div>

        <ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {CARDS.map((card, i) => (
            <li key={card.src} className={`${styleId}-scene`}>
              <button
                type="button"
                aria-label={`${card.title} — flip to read the caption`}
                className={`${styleId}-card group relative block aspect-[4/5] w-full rounded-2xl text-left 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`}
              >
                {/* front */}
                <span
                  className={`${styleId}-face absolute inset-0 overflow-hidden rounded-2xl shadow-lg shadow-neutral-900/10 ring-1 ring-neutral-900/5 dark:shadow-black/40 dark:ring-white/10`}
                >
                  {/* eslint-disable-next-line @next/next/no-img-element */}
                  <img
                    src={card.src}
                    alt={card.alt}
                    loading="lazy"
                    draggable={false}
                    className="h-full w-full object-cover"
                  />
                  <span className="pointer-events-none absolute inset-x-0 bottom-0 flex items-end justify-between gap-2 bg-gradient-to-t from-neutral-950/75 via-neutral-950/20 to-transparent p-4">
                    <span className="text-base font-semibold text-white drop-shadow">
                      {card.title}
                    </span>
                    <span className="rounded-full bg-white/15 px-2.5 py-1 text-[10px] font-medium uppercase tracking-wider text-white/90 backdrop-blur-sm">
                      Flip
                    </span>
                  </span>
                </span>

                {/* back */}
                <span
                  className={`${styleId}-face ${styleId}-back absolute inset-0 flex flex-col justify-between overflow-hidden rounded-2xl border border-neutral-200 bg-white p-6 shadow-lg shadow-neutral-900/10 dark:border-neutral-800 dark:bg-neutral-900 dark:shadow-black/40`}
                >
                  <span>
                    <span className="text-[10px] font-semibold uppercase tracking-[0.25em] text-indigo-600 dark:text-indigo-400">
                      Frame {String(i + 1).padStart(2, "0")}
                    </span>
                    <span className="mt-3 block text-xl font-bold text-neutral-900 dark:text-neutral-50">
                      {card.title}
                    </span>
                    <span className="mt-3 block text-sm leading-relaxed text-neutral-600 dark:text-neutral-300">
                      {card.caption}
                    </span>
                  </span>
                  <span className="mt-4 flex items-center justify-between border-t border-neutral-200 pt-3 text-xs text-neutral-500 dark:border-neutral-800 dark:text-neutral-400">
                    <span className="font-mono">{card.meta}</span>
                    <span className="text-indigo-600 dark:text-indigo-400">
                      ↺ flip back
                    </span>
                  </span>
                </span>
              </button>
            </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 →