Components / Hover Flip Card

Hover Flip Card

A hover-activated flip card that reveals a background image on the back, with the same content rendered on both sides for a seamless effect.

Preview

Hover over the card to flip between the content side and the image side.

Kendrick Lamar

Kendrick Lamar is a rapper from Compton, California. He is known for his unique style and flow.

Kendrick Lamar

Kendrick Lamar is a rapper from Compton, California. He is known for his unique style and flow.

Usage

tsx
import HoverFlipCard from "@/components/hover-flip-card/hover-flip-card"

export function Example() {
  return (
    <HoverFlipCard
      imageBackground="/images/photo-back.jpg"
      className="flex flex-col items-center justify-center p-6 rounded-xl bg-card/80 text-card-foreground shadow-lg backdrop-blur-sm"
    >
      <h2 className="text-lg font-semibold mb-2">Title</h2>
      <p className="text-sm text-muted-foreground">
        Front content goes here. On hover, the card flips to reveal the background image.
      </p>
    </HoverFlipCard>
  )
}

Props

PropTypeRequiredDescription
childrenReactNodeYesContent rendered on both front and back sides
imageBackgroundstringYesBackground image URL for the back side
classNamestringNoAdditional classes for inner content wrapper

Notes

  • Relies on custom 3D utility classes (e.g. transform-3d, rotate-y-180, backface-hidden).
  • Image side uses background-position that follows cursor for a subtle parallax effect.
  • Front and back share the same children for a consistent flip.

Source Code

tsx
'use client'
import { useState, useCallback, useMemo } from 'react'

interface HoverFlipCardProps {
    children: React.ReactNode
    imageBackground: string
    className?: string
}

const HoverFlipCard = ({ children, imageBackground, className }: HoverFlipCardProps) => {
    const [position, setPosition] = useState({ x: 50, y: 50 })
    const [isHovered, setIsHovered] = useState(false)

    const handleMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
        const { left, top, width, height } = e.currentTarget.getBoundingClientRect()
        const x = ((e.clientX - left) / width) * 100
        const y = ((e.clientY - top) / height) * 100
        setPosition({ x, y })
    }, [])

    const memoizedChildren = useMemo(() => children, [children])

    return (
        <div
            className="relative w-80 h-80 sm:w-96 sm:h-96 mx-auto perspective-[1000px]"
            onMouseMove={handleMouseMove}
            onMouseEnter={() => setIsHovered(true)}
            onMouseLeave={() => setIsHovered(false)}
        >
            <div
                className={`relative w-full h-full transition-transform duration-500 transform-3d ${isHovered ? 'rotate-y-180' : ''}`}
            >
                <div className={`absolute w-full h-full backface-hidden ${className}`}>
                    {memoizedChildren}
                </div>

                <div
                    className={`absolute w-full h-full backface-hidden rotate-y-180 bg-cover bg-center overflow-hidden ${className}`}
                    style={{
                        backgroundImage: `url(${imageBackground})`,
                        backgroundPosition: `${position.x}% ${position.y}%`
                    }}
                >
                    {memoizedChildren}
                </div>
            </div>
        </div>
    )
}

export default HoverFlipCard