Web InnoventixFreeCode

3D Flip Cards

Original · free

Pricing-style cards that flip in 3D on hover or tap to reveal details on the back, keyboard accessible with a staggered entrance animation.

byWeb InnoventixReact + Tailwind
flip card3d flip cardhover flip cardframer motion flipcard flip animationpricing flip card
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/card-flip.json
card-flip.tsx
"use client";

import { useState, type KeyboardEvent as ReactKeyboardEvent } from "react";
import { motion } from "motion/react";

const EASE = [0.16, 1, 0.3, 1] as const;

type FlipItem = {
  kicker: string;
  title: string;
  front: string;
  back: string;
  price: string;
  from: string;
  to: string;
};

const ITEMS: FlipItem[] = [
  {
    kicker: "Starter",
    title: "Launch kit",
    front: "Everything a small team needs to ship the first version and start learning fast.",
    back: "Unlimited projects, three seats, community support and a friendly onboarding call to get you moving.",
    price: "£19",
    from: "#6366f1",
    to: "#8b5cf6",
  },
  {
    kicker: "Growth",
    title: "Scale suite",
    front: "Automation, roles and analytics for teams that have found their rhythm.",
    back: "Ten seats, advanced permissions, priority support and reporting your stakeholders will actually read.",
    price: "£49",
    from: "#0ea5e9",
    to: "#22d3ee",
  },
  {
    kicker: "Studio",
    title: "Creator pack",
    front: "A polished toolkit for design-led teams who care about every pixel.",
    back: "Custom themes, brand tokens, motion presets and a shared library that keeps everyone on the same page.",
    price: "£39",
    from: "#f43f5e",
    to: "#fb923c",
  },
];

function FlipCard({ item, index }: { item: FlipItem; index: number }) {
  const [flipped, setFlipped] = useState(false);

  function onKey(e: ReactKeyboardEvent<HTMLDivElement>) {
    if (e.key === "Enter" || e.key === " ") {
      e.preventDefault();
      setFlipped((f) => !f);
    }
  }

  return (
    <motion.div
      initial={{ opacity: 0, y: 40 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, margin: "-60px" }}
      transition={{ duration: 0.6, delay: index * 0.12, ease: EASE }}
      style={{ perspective: 1300 }}
    >
      <div
        role="button"
        tabIndex={0}
        aria-pressed={flipped}
        aria-label={`${item.title}. Activate to see the details.`}
        onMouseEnter={() => setFlipped(true)}
        onMouseLeave={() => setFlipped(false)}
        onClick={() => setFlipped((f) => !f)}
        onKeyDown={onKey}
        className="relative h-80 w-full cursor-pointer rounded-3xl outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-4 focus-visible:ring-offset-zinc-50 dark:focus-visible:ring-offset-zinc-950"
      >
        <motion.div
          animate={{ rotateY: flipped ? 180 : 0 }}
          transition={{ duration: 0.7, ease: EASE }}
          style={{ transformStyle: "preserve-3d" }}
          className="relative h-full w-full"
        >
          {/* front */}
          <div
            style={{
              backfaceVisibility: "hidden",
              backgroundImage: `linear-gradient(135deg, ${item.from}, ${item.to})`,
            }}
            className="absolute inset-0 flex flex-col justify-between overflow-hidden rounded-3xl p-7 text-white shadow-xl shadow-zinc-900/10"
          >
            <div
              aria-hidden="true"
              className="pointer-events-none absolute -right-10 -top-10 h-40 w-40 rounded-full bg-white/20 blur-2xl"
            />
            <motion.div
              aria-hidden="true"
              animate={{ y: [0, -12, 0] }}
              transition={{ duration: 5, repeat: Infinity, ease: "easeInOut" }}
              className="pointer-events-none absolute bottom-8 right-6 h-16 w-16 rounded-2xl border border-white/30 bg-white/10 backdrop-blur-sm"
            />
            <div className="relative">
              <span className="inline-flex items-center gap-2 rounded-full bg-white/20 px-3 py-1 text-xs font-semibold uppercase tracking-widest backdrop-blur-sm">
                {item.kicker}
              </span>
              <h3 className="mt-5 text-2xl font-bold">{item.title}</h3>
              <p className="mt-3 max-w-[16rem] text-sm leading-relaxed text-white/85">
                {item.front}
              </p>
            </div>
            <div className="relative flex items-center gap-2 text-sm font-medium text-white/90">
              <svg
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                strokeWidth={2}
                strokeLinecap="round"
                strokeLinejoin="round"
                className="h-4 w-4"
                aria-hidden="true"
              >
                <path d="M3 12a9 9 0 1 0 9-9M3 12l3-3M3 12l3 3" />
              </svg>
              Hover or tap to flip
            </div>
          </div>

          {/* back */}
          <div
            style={{
              backfaceVisibility: "hidden",
              transform: "rotateY(180deg)",
            }}
            className="absolute inset-0 flex flex-col justify-between overflow-hidden rounded-3xl border border-zinc-200 bg-white p-7 shadow-xl shadow-zinc-900/10 dark:border-zinc-800 dark:bg-zinc-900"
          >
            <div
              aria-hidden="true"
              style={{ backgroundImage: `linear-gradient(135deg, ${item.from}, ${item.to})` }}
              className="pointer-events-none absolute -left-12 -top-12 h-40 w-40 rounded-full opacity-20 blur-2xl"
            />
            <div className="relative">
              <h3 className="text-lg font-bold text-zinc-900 dark:text-white">
                What is inside
              </h3>
              <p className="mt-3 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
                {item.back}
              </p>
            </div>
            <div className="relative flex items-end justify-between">
              <div>
                <div className="flex items-baseline gap-1">
                  <span className="text-3xl font-bold text-zinc-900 dark:text-white">
                    {item.price}
                  </span>
                  <span className="text-sm text-zinc-500 dark:text-zinc-500">
                    /month
                  </span>
                </div>
              </div>
              <span
                style={{ backgroundImage: `linear-gradient(135deg, ${item.from}, ${item.to})` }}
                className="inline-flex items-center gap-1.5 rounded-full px-4 py-2 text-sm font-semibold text-white shadow-lg"
              >
                Choose plan
                <svg
                  viewBox="0 0 24 24"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth={2}
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  className="h-4 w-4"
                  aria-hidden="true"
                >
                  <path d="M5 12h14M13 6l6 6-6 6" />
                </svg>
              </span>
            </div>
          </div>
        </motion.div>
      </div>
    </motion.div>
  );
}

export default function CardFlip() {
  return (
    <section className="bg-zinc-50 px-6 py-20 dark:bg-zinc-950 md:py-28">
      <div className="mx-auto max-w-6xl">
        <div className="mx-auto max-w-2xl text-center">
          <motion.h2
            initial={{ opacity: 0, y: 20 }}
            whileInView={{ opacity: 1, y: 0 }}
            viewport={{ once: true }}
            transition={{ duration: 0.6, ease: EASE }}
            className="text-balance text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl dark:text-white"
          >
            Two sides to every card
          </motion.h2>
          <motion.p
            initial={{ opacity: 0, y: 20 }}
            whileInView={{ opacity: 1, y: 0 }}
            viewport={{ once: true }}
            transition={{ duration: 0.6, delay: 0.1, ease: EASE }}
            className="mx-auto mt-4 max-w-lg text-zinc-600 dark:text-zinc-400"
          >
            Hover with a mouse or tap on touch to flip each card and reveal the
            detail behind the headline. Fully keyboard friendly too.
          </motion.p>
        </div>

        <div className="mt-14 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
          {ITEMS.map((item, i) => (
            <FlipCard key={item.title} item={item} index={i} />
          ))}
        </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 →
3D Tilt Cards

3D Tilt Cards

Original

A responsive grid of cards that tilt in 3D toward your cursor with a light glare and lifted depth layers, powered by Framer Motion springs.

Spotlight Follow Cards

Spotlight Follow Cards

Original

Feature cards where a soft radial spotlight and a glowing border chase the cursor on hover, with a staggered scroll reveal and an animated shimmer line.

Animated Gradient Border Cards

Animated Gradient Border Cards

Original

Cards wrapped in a live conic gradient border that rotates and speeds up on hover, with a shimmer sweep and a scrolling tag marquee.

Glow Effect Card

Glow Effect Card

primitives

A reusable glow effect card for React and Tailwind, with hover and motion.

Neon Gradient Card

Neon Gradient Card

gradient-card.tsx

A reusable neon gradient card for React and Tailwind, with hover and motion.

Spotlight Magic Card

Spotlight Magic Card

card.tsx

A reusable spotlight magic card for React and Tailwind, with hover and motion.

Tilt Spotlight Card

Tilt Spotlight Card

primitives

A reusable tilt spotlight card for React and Tailwind, with hover and motion.

Spotlight Hero

Spotlight Hero

Original

A centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero

Split Hero

Original

A two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero

Gradient Spotlight Hero

Original

A minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

App Preview Hero

App Preview Hero

Original

A centred hero that pairs headline copy with a realistic product dashboard mock built entirely from markup, complete with browser chrome and a floating notification card.

Waitlist Capture Hero

Waitlist Capture Hero

Original

A dark, focused hero with an inline email capture form and avatar social proof, ready for pre-launch waitlists.