# Flip Card

The FlipCard component creates an elegant, Apple-inspired interactive card that flips to reveal additional content. It features smooth animations, subtle gradients, and a clean design that's perfect for showcasing key information or features.

- **Category:** Card
- **Docs:** https://uibeats.com/docs/card/flip-card
- **Registry:** https://uibeats.com/r/flip-card.json
- **License:** MIT
- **npm dependencies:** `lucide-react`, `motion`

## Installation

```bash
npx shadcn@latest add https://uibeats.com/r/flip-card.json
```

Or install the dependencies and copy the source below:

```bash
npm install lucide-react motion
```

## When to use

Good when the back holds genuinely secondary detail: a definition, a statistic, a longer description. Never hide something essential behind a flip. Hover-triggered flips are unreachable on touch, so set triggerMode to 'click' if mobile matters.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `frontContent` | `{ title: string; subtitle?: string }` | `-` | Content to display on the front side of the card (required) |
| `backContent` | `{ title: string; description: string }` | `-` | Content to display on the back side of the card (required) |
| `width` | `string` | `'300px'` | Width of the flip card |
| `height` | `string` | `'200px'` | Height of the flip card |
| `flipDirection` | `'horizontal' \| 'vertical'` | `'horizontal'` | Direction of the flip animation |
| `triggerMode` | `'hover' \| 'click'` | `'hover'` | Event that triggers the flip animation |

## Example usage

```tsx
import React from "react";
import FlipCard from "@/components/demo/card/flip-card";

const FlipCardUsage = () => {
  return (
    <FlipCard
      frontContent={{
        title: "Discover",
        subtitle: "Hover to learn more",
      }}
      backContent={{
        title: "UI Beats",
        description:
          "A collection of modern and interactive UI components for React.",
      }}
      width="300px"
      height="200px"
      flipDirection="horizontal"
      triggerMode="hover"
    />
  );
};

export default FlipCardUsage;
```

## Source

Save as `components/ui/flip-card.tsx`.

```tsx
"use client";
import React, { useRef, useState } from "react";
import { motion, useReducedMotion } from "motion/react";
import { ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";

interface FlipCardProps {
  frontContent: {
    title: string;
    subtitle?: string;
  };
  backContent: {
    title: string;
    description: string;
  };
  width?: string;
  height?: string;
  flipDirection?: "horizontal" | "vertical";
  triggerMode?: "hover" | "click";
}

const FlipCard: React.FC<FlipCardProps> = ({
  frontContent,
  backContent,
  width = "300px",
  height = "200px",
  flipDirection = "horizontal",
  triggerMode = "hover",
}) => {
  const [isFlipped, setIsFlipped] = useState(false);
  const cardRef = useRef<HTMLDivElement>(null);
  const prefersReducedMotion = useReducedMotion();

  const toggleFlip = () => {
    if (triggerMode === "click") {
      setIsFlipped(!isFlipped);
    }
  };

  const rotateValue =
    flipDirection === "horizontal" ? "rotateY(180deg)" : "rotateX(180deg)";

  const cardStyle = {
    width,
    height,
    perspective: "1000px",
  };

  const faceClassNames = cn(
    "absolute flex h-full w-full flex-col items-center justify-center p-6",
    "rounded-2xl border border-border shadow-lg backface-hidden",
    "overflow-hidden",
  );

  return (
    <div
      ref={cardRef}
      style={cardStyle}
      onMouseEnter={() => {
        if (triggerMode === "hover") setIsFlipped(true);
      }}
      onMouseLeave={() => {
        if (triggerMode === "hover") setIsFlipped(false);
      }}
      onClick={toggleFlip}
      className="relative cursor-pointer rounded-2xl"
    >
      <motion.div
        className="relative h-full w-full rounded-2xl"
        style={{ transformStyle: "preserve-3d" }}
        animate={{ transform: isFlipped ? rotateValue : "none" }}
        /*
         * The card still turns under reduced motion; it just cuts rather than
         * sweeps. Suppressing the flip outright would leave the back face
         * permanently unreachable, which is a different bug from a calm one:
         * the component exists to show what is written on the other side.
         */
        transition={
          prefersReducedMotion
            ? { duration: 0 }
            : { duration: 0.6, ease: [0.23, 1, 0.32, 1] }
        }
      >
        <div
          className={cn(faceClassNames, "bg-gradient-to-br from-card to-muted")}
        >
          <h3 className="mb-2 text-2xl font-semibold text-card-foreground">
            {frontContent.title}
          </h3>
          {frontContent.subtitle && (
            <p className="text-sm text-muted-foreground">
              {frontContent.subtitle}
            </p>
          )}
          <ArrowRight className="absolute right-4 bottom-4 h-6 w-6 text-muted-foreground" />
        </div>
        <div
          className={cn(faceClassNames, "bg-gradient-to-br from-muted to-card")}
          style={{ transform: rotateValue, backfaceVisibility: "hidden" }}
        >
          <h3 className="mb-3 text-xl font-semibold text-card-foreground">
            {backContent.title}
          </h3>
          <p className="text-center text-sm text-muted-foreground">
            {backContent.description}
          </p>
        </div>
      </motion.div>
    </div>
  );
};

export default FlipCard;
```

## Credits

Written by [Michael](https://github.com/lappemic).
