# Flip Card (/docs/display/flip-card)



<ComponentShowcase name="flip-card" />

## Installation [#installation]

```bash
npx shadcn@latest add https://ui.zyeon.ai/r/flip-card.json
```

## Prompt [#prompt]

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

```text
Build a React + TypeScript + Tailwind "FlipCard" component (no runtime
dependencies beyond React and the shared cn() utility).

Contract
- Export a forwardRef<HTMLDivElement, FlipCardProps> extending
  React.HTMLAttributes<HTMLDivElement>; spread the rest onto the root.
- Props: front and back (ReactNode, required), trigger = "hover" | "click"
  (default "hover"), axis = "x" | "y" (default "y"), duration = 600 (ms),
  and the controlled pair flipped?: boolean + onFlippedChange?: (b) => void.
- Uncontrolled by default: keep internal state, and treat `flipped !==
  undefined` as controlled. Notify onFlippedChange on every intended change
  in both modes, and skip the call when the value would not change (focusin
  can fire repeatedly).
- The faces are deliberately unstyled — pass fully formed cards as front /
  back. The component owns geometry, triggers and a11y, not the visuals.

Behavior
- trigger="hover": pointerenter/pointerleave flip it, and focusin/focusout
  do the same so keyboard users can reach the back at all; the root is
  tabbable for that reason. In the blur handler, ignore focusout events
  whose relatedTarget is still inside the card, otherwise moving focus
  between two children flickers the card back and forth.
- trigger="click": the root becomes role="button" + tabIndex + aria-pressed
  and toggles on click, Enter and Space (preventDefault on Space so the page
  does not scroll). A real <button> element is intentionally NOT used: the
  back face usually contains links or buttons, and interactive content
  inside a <button> is invalid HTML. For the same reason the key handler
  only fires when event.target === event.currentTarget — without that check
  it would preventDefault a descendant button's Enter and flip the card
  instead of activating the button. Clicks still bubble by design (the whole
  face must be clickable), so an interactive child that should not also flip
  the card calls stopPropagation() in its own handler.
- Both faces live in the same CSS grid cell (col-start-1 row-start-1), so
  the card is exactly as tall as its tallest face and no fixed height has to
  be maintained.
- 3D mode: the flipper carries transform-3d plus a transition-transform of
  `duration`; the back face is pre-rotated 180° on the chosen axis and both
  faces are backface-hidden, so exactly one face ever paints.
- prefers-reduced-motion: swap the rotation for an opacity cross-fade of the
  same duration and drop the perspective entirely. Flipping still works —
  only the 3D motion is removed.
- The face that is turned away gets aria-hidden, `inert` and
  pointer-events-none the moment the state changes, so it can never be
  clicked or tabbed into while it is out of sight ("invisible but
  focusable" is the classic flip-card bug). The trade-off is honest: a
  screen reader only ever hears the face that is showing, which is why the
  root carries an sr-only line stating which face is up and how to flip it.

Rendering & styling
- Root: relative w-full, perspective-[1200px] (omitted under reduced
  motion), rounded-xl focus-visible:ring-2 focus-visible:ring-ring
  focus-visible:ring-offset-2, cursor-pointer only in click mode.
- Faces: col-start-1 row-start-1 with a *:h-full so both faces fill the
  shared cell no matter which one is taller.
- No color of its own beyond what the caller passes — semantic tokens live
  in the front / back nodes (bg-card, border, bg-muted for the reverse).
- Merge the consumer className through cn().

Customization levers
- Trigger: "hover" for marketing grids on desktop, "click" when the card
  must work on touch — a finger cannot hover, so hover mode is desktop-only
  by nature.
- axis="x" for a vertical tumble (reads well on wide, short cards),
  axis="y" for the classic left/right spin.
- duration: 400–500ms feels snappy, 700–900ms feels heavy and premium.
- Controlled mode: drive `flipped` from a parent to flip several cards at
  once, to reset a grid, or to put the trigger on a dedicated button (do
  that when the back face has its own buttons, so the card itself stops
  being one big toggle).
- Face design: any markup; give both faces the same padding and radius so
  the turn does not visibly resize the card, and keep them within ~1.5x of
  each other in height or the front face will look padded out.
```

## Concepts [#concepts]

<Mermaid
  chart="`flowchart TD
A[&#x22;trigger&#x22;] --> B{&#x22;hover or click?&#x22;}
B -- hover --> C[&#x22;pointerenter / focusin → flipped<br/>focusout ignored when it stays inside&#x22;]
B -- click --> D[&#x22;click / Enter / Space<br/>role=button + aria-pressed&#x22;]
C --> E{&#x22;prefers-reduced-motion?&#x22;}
D --> E
E -- no --> F[&#x22;flipper rotates 180° on axis<br/>both faces backface-hidden&#x22;]
E -- yes --> G[&#x22;opacity cross-fade<br/>no perspective, same duration&#x22;]
F --> H[&#x22;hidden face: aria-hidden + inert<br/>+ pointer-events-none&#x22;]
G --> H
I[&#x22;both faces share one grid cell&#x22;] --> J[&#x22;height = tallest face<br/>no fixed height&#x22;]`"
/>

* **Two faces, one grid cell** — stacking both faces at `col-start-1 row-start-1` makes the card as tall as its tallest side, which is why this container never needs a hardcoded height.
* **Backface-hidden pairing** — the flipper rotates, the back face is pre-rotated by the same 180°, and both faces hide their backside, so exactly one side ever paints and the two coplanar faces never fight.
* **Hover means hover-or-focus** — a hover-only reveal is invisible to keyboard users, so `focusin`/`focusout` drive the same state; `relatedTarget` filtering keeps focus moves inside the card from flickering it.
* **Inert hidden face** — the turned-away face is `aria-hidden` *and* `inert`, closing the "can't see it, can still Tab into it" hole that most CSS-only flip cards leave open.
* **Controlled flip** — `flipped` + `onFlippedChange` let a parent own the state: flip a whole grid at once, reset on route change, or move the trigger onto a dedicated button when the back face has its own actions.
* **Reduced-motion cross-fade** — under `prefers-reduced-motion` the rotation becomes an opacity swap and the perspective is dropped; the feature is intact, only the spinning is gone.
