---
name: building-flip-card-hero
description: Use when a landing page hero needs a left/right split (headline and CTAs on the left, a main image or visual on the right), or when asked for a hero image or card that flips, turns over or animates between two states on click and on its own.
---

# Building a flip-card hero

## Overview

The hero is two columns: the pitch on the left, and on the right a card whose two faces tell the product's story as a before and after. The card flips on click, from its own button, and on a timer. Front = the thing the user has today (a paper form, a messy pile, a raw document). Back = what the product turns it into (structured data, a checklist, scored fields).

## Hero contract

```
lg: grid-cols-[minmax(0,5fr)_minmax(0,6fr)] items-center   (stacks below lg)
┌──────────────────────────┬──────────────────────────────┐
│ h1 (one sentence)        │ <FlipCard front back />      │
│ p  (2–4 lines, max-w-xl) │                              │
│ [Primary CTA] [Try demo] │  ↻ Show the extracted data   │
└──────────────────────────┴──────────────────────────────┘
```

- The second CTA scrolls to a full interactive demo in its own section **directly under the hero**. If the page already had a big demo in the hero, move it there instead of deleting it.
- Faces are built from the product's real sample data (shared module), never placeholder images.
- Both faces use the site's tokens and fill the same box (`h-full`). The shorter face centers its content (`flex flex-col justify-center`).

## The component

Copy `flip-card.tsx` (next to this file) into the repo's components folder. It provides:

| Behavior         | How                                                                                                                                                                      |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Flip on click    | Clicks on the card flip it unless the target is a link, button or input                                                                                                  |
| Keyboard path    | A visible button below the card, labeled with the face it will show (`aria-pressed`)                                                                                     |
| Auto flip        | `setInterval` (default 4.5s), paused while hovered or focused                                                                                                            |
| Reduced motion   | No auto flip. The click flip becomes instant only if the site's global CSS disables transitions under `prefers-reduced-motion: reduce`, so add that rule if it's missing |
| Hidden face      | `inert`, so its links and inputs can't be tabbed to                                                                                                                      |
| Content per flip | `onFlip(showingBack)` callback                                                                                                                                           |

Cycling through several samples: keep `frontIndex` and `backIndex` in separate state. Set `backIndex = frontIndex` when flipping to the back, and advance `frontIndex` when flipping to the front, so neither face changes while it's visible. Render every sample on each face, stacked in one grid cell with all but the active one `invisible` and `inert`, so the card is always as tall as the tallest sample and the hero doesn't jump as it rotates.

Requires Tailwind v4 (`perspective-*`, `transform-3d`, `backface-hidden`, `rotate-y-180`). On Tailwind v3, write those four as plain CSS.

## Verification

1. Lint, type-check, build.
2. At 1440px: screenshot the front; wait past the interval and check that `getComputedStyle(card).transform` is a `matrix3d(-1, …)`; screenshot the back.
3. At 390px: `document.documentElement.scrollWidth <= innerWidth`, and the card stacks below the text.
4. Check each scroll container inside a face: `scrollWidth === clientWidth`.

## Common mistakes

| Symptom                              | Cause                                                                        | Fix                                                                                                                                              |
| ------------------------------------ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Page scrolls sideways on phones      | Grid columns default to `auto` and grow to a table's min width               | The component uses `grid-cols-[minmax(0,1fr)]` and `min-w-0`; keep them                                                                          |
| Stray horizontal scrollbar in a face | Highlight spans with `-mx-1` overhang the last column                        | Add `pr-1` to the scroll wrapper                                                                                                                 |
| Big empty area on one face           | Faces share the height of the taller one                                     | Center the shorter face, then even the sizes (taller form boxes, tighter data rows) until both faces' content is within ~40px of the card height |
| Hero jumps as samples rotate         | Each sample has its own height                                               | Stack every sample in one grid cell and show only the active one (above)                                                                         |
| Content changes mid-rotation         | Both faces read one index                                                    | Separate front and back indices (above)                                                                                                          |
| Side effects run twice               | Callback called inside a `setState` updater                                  | Compute `next`, call `setFlipped(next)`, then `onFlip(next)`                                                                                     |
| Screenshots show stale content       | Dev server served a cached build, or a parallel agent drove the same browser | Reload, re-screenshot, and don't share the browser with other agents                                                                             |
