---
name: hover-flip-card
description: Use when you want "Interactive, playful, familiar" - Flips a card to reveal hidden content on the back.
---

# Hover Flip Card

> **Category:** Card / Layout  -  **Personality:** Interactive, playful, familiar
>
> **Best use:** Team cards, service cards, quizzes
>
> **Ratings:** Professional 3/5 - Casual 5/5 - Exotic 2/5 - Visual impact 3/5 - Difficulty 2/5 - Performance cost 2/5

## What it is

A flip card shows a compact **front** (here: a team member's photo, name and role) and, on hover or keyboard focus, rotates 180&deg; in 3D to reveal a **back** with the detail you didn't have room for (bio, skill tags, a contact button). It's two stacked faces inside a `preserve-3d` wrapper; `backface-visibility: hidden` keeps whichever face is turned away from showing through. Use it for team grids, service cards and quiz/flashcard UIs where you want a tidy grid up front but a payoff on interaction.

## Dependencies / CDN

None - pure CSS for the effect, plus a few lines of vanilla JS only to keep the hidden back face out of the keyboard tab order. Demo fonts are optional:

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Familjen+Grotesk:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
```

## HTML

The card is one focusable element wrapping a flip layer with two faces:

```html
<article class="card" tabindex="0" role="group" aria-label="Noor Haddad, Creative Director">
  <div class="flip">
    <div class="face front">
      <img src="/img/portrait-01.jpg" alt="Portrait of Noor Haddad">
      <span class="hint" aria-hidden="true">&#8635; flip</span>
      <div class="cap">
        <p class="role">Creative Director</p>
        <h3 class="name">Noor Haddad</h3>
      </div>
    </div>
    <div class="face back">
      <h3 class="name">Noor Haddad</h3>
      <p class="bio">Leads brand and identity work&hellip;</p>
      <ul class="tags"><li>Brand</li><li>Typography</li><li>Art Direction</li></ul>
      <a class="cta" href="#">Get in touch &#8599;</a>
    </div>
  </div>
</article>
```

## CSS

Only these rules are load-bearing - everything else is dressing:

```css
.card{                                /* 1) perspective lives on the PARENT */
  position: relative; perspective: 1500px;
  height: 420px; cursor: pointer; outline: none;
}
.flip{                                /* 2) the layer that actually rotates */
  position: absolute; inset: 0;
  transform-style: preserve-3d;       /*    keep the two faces in real 3D space */
  will-change: transform;
  transition: transform .72s cubic-bezier(.2,.72,.2,1.02);
}
.card:hover .flip,
.card:focus-within .flip{ transform: rotateY(180deg); }   /* 3) flip on hover OR focus */

.face{                                /* 4) stack both faces, hide the far side */
  position: absolute; inset: 0; overflow: hidden; border-radius: 22px;
  -webkit-backface-visibility: hidden;        /* Safari/iOS need the prefix */
          backface-visibility: hidden;
}
.back{ transform: rotateY(180deg); }  /* 5) pre-rotate the back so it faces us after the flip */

.card:focus-visible{ outline: 3px solid #ff6a3d; outline-offset: 4px; }  /* keyboard ring */
```

## JavaScript

The flip is pure CSS; JS only removes the hidden back's links from the tab order until the card is active, then restores them:

```js
document.querySelectorAll('.card').forEach(function(card){
  var targets = card.querySelectorAll('.back a, .back button');
  var setActive = function(on){
    targets.forEach(function(el){ el.tabIndex = on ? 0 : -1; });
  };
  setActive(false);                                  // start hidden -> not tabbable
  card.addEventListener('pointerenter', function(){ setActive(true); });
  card.addEventListener('pointerleave', function(){ if(!card.matches(':focus-within')) setActive(false); });
  card.addEventListener('focusin',  function(){ setActive(true); });          // keyboard in
  card.addEventListener('focusout', function(e){ if(!card.contains(e.relatedTarget)) setActive(false); });
});
```

## How it works

- **`perspective` on the parent** gives the rotation depth; lower values exaggerate the 3D, higher values flatten it.
- **`.flip` is the thing that rotates**, and `transform-style: preserve-3d` lets its two child faces live in the same 3D space instead of being flattened to a single plane.
- Both `.face`s are stacked with `position: absolute; inset: 0`. **`backface-visibility: hidden`** makes each face invisible while it points away from you, so only one is ever seen.
- The **back is pre-rotated `rotateY(180deg)`**. When the wrapper flips 180&deg;, the front turns away (and vanishes) while the back swings around to face you.
- **`:hover`** covers pointer; **`:focus-within`** covers keyboard - the card is `tabindex="0"`, and `:focus-within` also keeps it flipped while a button on the back has focus.

## Customization

- **Axis:** swap `rotateY` for `rotateX` to flip vertically (top-over).
- **Feel:** tune the `transition` duration and easing; the slight overshoot (`cubic-bezier(...,1.02)`) adds a playful snap - drop it for a calmer, corporate flip.
- **Depth:** `perspective: 900px` makes it dramatic; `2000px` makes it subtle.
- **Trigger:** for touch, drop `:focus-within` and toggle an `.is-flipped` class on `click`/`Enter` instead of relying on hover.
- Re-skin freely (palette, photo vs. solid front, icon back) - the five numbered rules above are all that the effect needs.

## Accessibility & performance

- Make the card **focusable** (`tabindex="0"`) with a clear **`:focus-visible`** ring so the reveal is reachable without a mouse.
- Both faces are real DOM text, so screen readers read the back's content regardless of the visual rotation - but the back's *links* are visually hidden, so **remove them from the tab order until the card is active** (the JS above) to avoid phantom focus stops on an unseen face.
- **Honour reduced motion:** `@media (prefers-reduced-motion: reduce){ .flip{ transition: none } }` - the card still reveals the back, just instantly instead of spinning.
- Cheap to run: a single GPU-composited `transform`. Add `will-change: transform` to the flipping layer; never animate `width`/`height`/`box-shadow` during the flip.

## Gotchas

- **Forgetting the `-webkit-` prefix** on `backface-visibility` -> both faces bleed through each other on Safari/iOS.
- **`overflow: hidden` on the flipping wrapper** clips the 3D and flattens it - put `overflow: hidden` on each `.face` (for the rounded corners), not on `.flip`.
- **No `transform-style: preserve-3d`** (or an ancestor `filter`/`opacity`/`overflow` that quietly resets it) collapses the scene to 2D and the back never hides.
- **Each face needs a solid background**; a transparent face lets the mirror-image back text show through the front.
- **No hover on touch** - provide a tap/click toggle, or the back content is unreachable on phones.
