Flip Card

A card with two faces that turns in 3D when it is clicked, controlled or on its own.

Loading preview

Installation

npx shadcn@latest add @loomui/flip-card

Usage

import { FlipCard } from "@/components/ui/flip-card"
<FlipCard
  front={
    <div className="bg-card grid size-24 place-items-center rounded-xl border">
      ?
    </div>
  }
  back={
    <div className="bg-card grid size-24 place-items-center rounded-xl border">
      🍑
    </div>
  }
/>

The front face sits in normal flow and gives the card its size. The back is laid over it and pre-turned, so the two faces always match dimensions without either being measured.

The turn is one transform on one element, eased with --ease-back-out, which overshoots a few degrees and settles. That overshoot is the whole difference between a card being thrown over and a card being rotated by a computer.

Driving it yourself

Left alone, the card owns its state and every click turns it. Pass flipped and it does what you say instead. That is what a game of pairs needs. A card turns face up when you pick it, and turns back when the pair misses.

const [flipped, setFlipped] = React.useState(false)
 
<FlipCard flipped={flipped} onClick={() => setFlipped(true)} front={...} back={...} />

onFlippedChange reports the face the card is turning to. It fires in both modes, so it is the right place to hang a sound or a counter.

Axis and weight

axis="x" turns the card top over bottom instead of left over right. depth is the perspective in pixels. Lower numbers exaggerate the near edge and make the card feel thrown at you. Higher numbers flatten it out.

<FlipCard axis="x" depth={600} duration={560} front={...} back={...} />

Under prefers-reduced-motion the turn is dropped and the faces swap instantly.

Props

PropTypeDefaultDescription
frontReactNode-Face shown at rest. It sets the card's size.
backReactNode-Face shown once the card is turned.
flippedbooleannoneDrive the card yourself. Omit to let it own this.
defaultFlippedbooleanfalseStarting face when the card owns its state.
onFlippedChange(flipped: boolean) => voidnoneCalled with the face the card is turning to.
axis"x" | "y""y"Turn top over bottom, or left over right.
durationnumber480Length of the turn in milliseconds.
depthnumber900Perspective in pixels. Lower is deeper.
classNamestringnoneMerged onto the card. Size and layout here.

Any other <button> prop is forwarded, disabled included.

Accessibility

The card is a real <button> with aria-pressed, so it is reachable by keyboard, announces which way it is turned, and needs no invented role. Both faces stay in the DOM. Whichever one is turned away is aria-hidden, so a screen reader reads the face you are looking at instead of both at once.