Studio KitHigh-End Animated Cardsmotion/react

FlipCard

3D card that flips on a tuned spring around the Y or X axis to reveal a back face, triggered by hover or click and always by Enter/Space.

Preview

Flip Card

Hover, click or press Enter to turn me over.

Customize

8 props
288
224

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add flip-card

Writes components/ui/flip-card.tsx and components/styles/flip-card.module.css.

Usage

Updates live as you change the controls — only props that differ from the defaults are included.

example.tsx
1import { FlipCard } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <FlipCard />6  );7}

Props

PropTypeDefault
front

Custom front face content (overrides frontTitle/frontText).

React.ReactNodeundefined
back

Custom back face content (overrides backTitle/backText).

React.ReactNodeundefined
frontTitle

Front heading used when front is not provided.

string'Flip Card'
frontText

Front text used when front is not provided.

string'Hover, click or press Enter to turn me over.'
backTitle

Back heading used when back is not provided.

string'Spring-driven 3D'
backText

Back text used when back is not provided.

string'Perspective, backface culling and a tuned spring. Press Enter again to flip back.'
trigger

Pointer interaction that flips the card (Enter/Space always flips).

'hover' | 'click''hover'
axis

Rotation axis.

'y' | 'x''y'
width

Card width in pixels.

number288
height

Card height in pixels.

number224
className

Extra class merged onto the root.

string''

Accessibility

Root is a focusable role="button" with aria-pressed reflecting the flipped state; Enter and Space flip it. The hidden face is aria-hidden so only the visible side is read. Hover-trigger cards also flip on touch tap, and prefers-reduced-motion swaps the spring for an instant flip.

Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.

Source

components/ui/flip-card.tsx
1"use client";2 3import * as React from "react";4import { motion, useReducedMotion } from "motion/react";5import styles from "../styles/flip-card.module.css";6 7export interface FlipCardProps {8  /** Custom front face content (overrides frontTitle/frontText) */9  front?: React.ReactNode;10  /** Custom back face content (overrides backTitle/backText) */11  back?: React.ReactNode;12  /** Front face heading used when `front` is not provided */13  frontTitle?: string;14  /** Front face text used when `front` is not provided */15  frontText?: string;16  /** Back face heading used when `back` is not provided */17  backTitle?: string;18  /** Back face text used when `back` is not provided */19  backText?: string;20  /** What flips the card with a pointer (Enter/Space always flips) */21  trigger?: "hover" | "click";22  /** Rotation axis */23  axis?: "y" | "x";24  /** Card width in pixels */25  width?: number;26  /** Card height in pixels */27  height?: number;28  /** Optional className */29  className?: string;30}31 32export function FlipCard({33  front,34  back,35  frontTitle = "Flip Card",36  frontText = "Hover, click or press Enter to turn me over.",37  backTitle = "Spring-driven 3D",38  backText = "Perspective, backface culling and a tuned spring. Press Enter again to flip back.",39  trigger = "hover",40  axis = "y",41  width = 288,42  height = 224,43  className = "",44}: FlipCardProps) {45  const prefersReducedMotion = useReducedMotion();46  const [flipped, setFlipped] = React.useState(false);47  const lastPointer = React.useRef("mouse");48  const rotateKey = axis === "x" ? "rotateX" : "rotateY";49 50  const onPointerEnter = (e: React.PointerEvent<HTMLDivElement>) => {51    lastPointer.current = e.pointerType;52    if (trigger === "hover" && e.pointerType !== "touch") setFlipped(true);53  };54  const onPointerLeave = (e: React.PointerEvent<HTMLDivElement>) => {55    if (trigger === "hover" && e.pointerType !== "touch") setFlipped(false);56  };57  const onClick = () => {58    if (trigger === "click" || lastPointer.current === "touch") setFlipped((f) => !f);59  };60  const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {61    if (e.key === "Enter" || e.key === " ") {62      e.preventDefault();63      setFlipped((f) => !f);64    }65  };66 67  return (68    <div69      role="button"70      tabIndex={0}71      aria-pressed={flipped}72      aria-roledescription="flip card"73      onPointerEnter={onPointerEnter}74      onPointerDown={(e) => {75        lastPointer.current = e.pointerType;76      }}77      onPointerLeave={onPointerLeave}78      onClick={onClick}79      onKeyDown={onKeyDown}80      style={81        {82          "--mf-fc-w": `${width}px`,83          "--mf-fc-h": `${height}px`,84        } as React.CSSProperties85      }86      className={`${styles.root} ${className}`.trim()}87    >88      <motion.div89        className={styles.inner}90        initial={false}91        animate={{ [rotateKey]: flipped ? 180 : 0 }}92        transition={prefersReducedMotion ? { duration: 0 } : { type: "spring", stiffness: 120, damping: 16 }}93      >94        <div className={`${styles.face} ${styles.front}`} aria-hidden={flipped}>95          {front ?? (96            <>97              <span className={styles.badge} aria-hidden="true">98                <svg viewBox="0 0 24 24" width="18" height="18">99                  <path d="M4 12a8 8 0 0 1 14-5.3M20 12a8 8 0 0 1-14 5.3M18 3v4h-4M6 21v-4h4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />100                </svg>101              </span>102              <h3 className={styles.title}>{frontTitle}</h3>103              <p className={styles.text}>{frontText}</p>104            </>105          )}106        </div>107        <div108          className={`${styles.face} ${styles.back}`}109          aria-hidden={!flipped}110          style={{ transform: axis === "x" ? "rotateX(180deg)" : "rotateY(180deg)" }}111        >112          {back ?? (113            <>114              <h3 className={styles.title}>{backTitle}</h3>115              <p className={styles.text}>{backText}</p>116            </>117          )}118        </div>119      </motion.div>120    </div>121  );122}