Web InnoventixFreeCode

Click Flip Effect

Original · free

Click flips the image card to details.

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

import { useState } from "react";
import { motion, useReducedMotion } from "motion/react";

type FlipCard = {
  src: string;
  alt: string;
  title: string;
  location: string;
  meta: { label: string; value: string }[];
  note: string;
};

const CARDS: FlipCard[] = [
  {
    src: "/img/gallery/g07.webp",
    alt: "Sunlit modern interior with layered shadows falling across a plaster wall",
    title: "Northlight Study",
    location: "Loft No. 4",
    meta: [
      { label: "Shot on", value: "50mm f/1.8" },
      { label: "Light", value: "Window, 4pm" },
      { label: "Edit", value: "Warm, low grain" },
    ],
    note: "Bounced the afternoon sun off a single plaster wall to keep the shadows soft.",
  },
  {
    src: "/img/gallery/g19.webp",
    alt: "Quiet architectural facade with strong geometric lines under an open sky",
    title: "Concrete & Sky",
    location: "West Terrace",
    meta: [
      { label: "Shot on", value: "24mm f/8" },
      { label: "Light", value: "Overcast noon" },
      { label: "Edit", value: "High contrast" },
    ],
    note: "Waited out the clouds so the facade read as pure line against flat grey.",
  },
];

export default function ImgfxClickFlip() {
  const reduce = useReducedMotion();
  const [flipped, setFlipped] = useState<Record<number, boolean>>({});

  const toggle = (i: number) =>
    setFlipped((prev) => ({ ...prev, [i]: !prev[i] }));

  return (
    <section className="relative w-full overflow-hidden bg-neutral-50 px-6 py-20 text-neutral-900 sm:py-28 dark:bg-neutral-950 dark:text-neutral-100">
      <style>{`
        @keyframes imgfxClickFlip_hint {
          0%, 100% { transform: translateY(0); opacity: 0.55; }
          50% { transform: translateY(3px); opacity: 1; }
        }
        .imgfxClickFlip_pulse {
          animation: imgfxClickFlip_hint 2.4s ease-in-out infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .imgfxClickFlip_pulse { animation: none !important; }
          .imgfxClickFlip_inner { transition: none !important; }
        }
      `}</style>

      <div className="mx-auto max-w-5xl">
        <motion.div
          initial={reduce ? false : { opacity: 0, y: 16 }}
          whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
          viewport={{ once: true, margin: "-80px" }}
          transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
          className="mb-12 text-center"
        >
          <p className="mb-3 text-xs font-semibold uppercase tracking-[0.25em] text-indigo-600 dark:text-indigo-400">
            Click to Flip
          </p>
          <h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">
            The story lives on the back
          </h2>
          <p className="mx-auto mt-4 max-w-md text-sm text-neutral-600 dark:text-neutral-400">
            Tap any frame to turn it over and read the shot notes.
          </p>
        </motion.div>

        <div className="grid gap-8 sm:grid-cols-2">
          {CARDS.map((card, i) => {
            const isFlipped = !!flipped[i];
            return (
              <motion.div
                key={card.src}
                initial={reduce ? false : { opacity: 0, y: 24 }}
                whileInView={reduce ? undefined : { opacity: 1, y: 0 }}
                viewport={{ once: true, margin: "-60px" }}
                transition={{
                  duration: 0.6,
                  delay: i * 0.1,
                  ease: [0.22, 1, 0.36, 1],
                }}
                className="[perspective:1400px]"
              >
                <button
                  type="button"
                  onClick={() => toggle(i)}
                  aria-pressed={isFlipped}
                  aria-label={
                    isFlipped
                      ? `Show photo: ${card.title}`
                      : `Show details for ${card.title}`
                  }
                  className="group relative block aspect-[4/5] 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"
                >
                  <div
                    className="imgfxClickFlip_inner relative h-full w-full rounded-2xl shadow-lg shadow-neutral-900/10 transition-transform duration-700 ease-[cubic-bezier(0.22,1,0.36,1)] [transform-style:preserve-3d] dark:shadow-black/40"
                    style={{
                      transform: isFlipped ? "rotateY(180deg)" : "rotateY(0deg)",
                    }}
                  >
                    {/* FRONT */}
                    <div className="absolute inset-0 overflow-hidden rounded-2xl [backface-visibility:hidden]">
                      {/* 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 transition-transform duration-700 ease-out group-hover:scale-[1.03]"
                      />
                      <div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-neutral-950/70 via-neutral-950/10 to-transparent" />
                      <div className="absolute inset-x-0 bottom-0 flex items-end justify-between gap-3 p-4">
                        <div className="text-left">
                          <p className="text-base font-semibold text-white drop-shadow-sm">
                            {card.title}
                          </p>
                          <p className="text-xs text-white/75">
                            {card.location}
                          </p>
                        </div>
                        <span className="imgfxClickFlip_pulse rounded-full bg-white/15 px-3 py-1 text-[11px] font-medium text-white ring-1 ring-white/25 backdrop-blur-sm">
                          Flip
                        </span>
                      </div>
                    </div>

                    {/* BACK */}
                    <div className="absolute inset-0 flex flex-col justify-between overflow-hidden rounded-2xl border border-neutral-200 bg-gradient-to-br from-white to-neutral-100 p-6 text-left [backface-visibility:hidden] [transform:rotateY(180deg)] dark:border-neutral-800 dark:from-neutral-900 dark:to-neutral-950">
                      <div>
                        <p className="text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
                          {card.location}
                        </p>
                        <h3 className="mt-1 text-2xl font-semibold tracking-tight">
                          {card.title}
                        </h3>
                        <dl className="mt-5 space-y-3">
                          {card.meta.map((m) => (
                            <div
                              key={m.label}
                              className="flex items-baseline justify-between border-b border-dashed border-neutral-200 pb-2 dark:border-neutral-800"
                            >
                              <dt className="text-xs uppercase tracking-wide text-neutral-500 dark:text-neutral-400">
                                {m.label}
                              </dt>
                              <dd className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
                                {m.value}
                              </dd>
                            </div>
                          ))}
                        </dl>
                      </div>
                      <p className="mt-4 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
                        {card.note}
                      </p>
                      <span className="mt-4 inline-flex w-fit items-center gap-1 rounded-full bg-indigo-50 px-3 py-1 text-[11px] font-medium text-indigo-700 ring-1 ring-indigo-200 dark:bg-indigo-500/10 dark:text-indigo-300 dark:ring-indigo-500/30">
                        Flip back
                      </span>
                    </div>
                  </div>
                </button>
              </motion.div>
            );
          })}
        </div>
      </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 →