Pure TailwindMIT licensed

Tailwind 3D Hover Flip Card Grid

A flashcard wall where hovering a tile rotates it 180° to reveal an accent-coloured back face of metrics, and the grid never shifts because only a transform animates. v4 ships perspective-*, transform-3d, backface-hidden and rotate-y-* as real utilities, so the entire 3D scene is expressible without touching the stylesheet.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tcg-06 w-full min-h-svh bg-tcg06-bg font-tcg06-sans text-white px-6 py-12" aria-label="3D hover flip card grid demo">
  <div class="max-w-[780px] mx-auto grid [grid-template-columns:repeat(auto-fit,minmax(160px,1fr))] gap-5">

    <div class="group perspective-[1000px] h-40">
      <div class="relative w-full h-full transform-3d transition-transform duration-600 ease-tcg06 rotate-y-180 group-hover:rotate-y-0 group-focus-within:rotate-y-0 motion-reduce:transition-none motion-reduce:rotate-y-0">
        <div class="absolute inset-0 flex flex-col justify-end gap-1.5 p-4.5 rounded-[14px] backface-hidden border border-white/10 bg-tcg06-face">
          <span class="text-[.7rem] font-semibold tracking-[0.1em] uppercase text-tcg06-dim">Latency</span>
          <h3 class="text-[1.2rem] font-bold">API</h3>
        </div>
        <div style="--accent:#7c6af7" class="absolute inset-0 flex flex-col justify-end gap-1 p-4.5 rounded-[14px] backface-hidden rotate-y-180 bg-[var(--accent)] text-tcg06-bg">
          <span class="text-[1.5rem] font-extrabold tracking-[-0.02em] tabular-nums">42ms</span>
          <p class="text-[.78rem] font-semibold opacity-80">p95 response time</p>
        </div>
      </div>
    </div>

    <div class="group perspective-[1000px] h-40">
      <div class="relative w-full h-full transform-3d transition-transform duration-600 ease-tcg06 group-hover:rotate-y-180 group-focus-within:rotate-y-180 motion-reduce:transition-none motion-reduce:group-hover:rotate-y-0">
        <div class="absolute inset-0 flex flex-col justify-end gap-1.5 p-4.5 rounded-[14px] backface-hidden border border-white/10 bg-tcg06-face">
          <span class="text-[.7rem] font-semibold tracking-[0.1em] uppercase text-tcg06-dim">Uptime</span>
          <h3 class="text-[1.2rem] font-bold">SLA</h3>
        </div>
        <div style="--accent:#00c2a8" class="absolute inset-0 flex flex-col justify-end gap-1 p-4.5 rounded-[14px] backface-hidden rotate-y-180 bg-[var(--accent)] text-tcg06-bg">
          <span class="text-[1.5rem] font-extrabold tracking-[-0.02em] tabular-nums">99.98%</span>
          <p class="text-[.78rem] font-semibold opacity-80">last 90 days</p>
        </div>
      </div>
    </div>

    <div class="group perspective-[1000px] h-40">
      <div class="relative w-full h-full transform-3d transition-transform duration-600 ease-tcg06 group-hover:rotate-y-180 group-focus-within:rotate-y-180 motion-reduce:transition-none motion-reduce:group-hover:rotate-y-0">
        <div class="absolute inset-0 flex flex-col justify-end gap-1.5 p-4.5 rounded-[14px] backface-hidden border border-white/10 bg-tcg06-face">
          <span class="text-[.7rem] font-semibold tracking-[0.1em] uppercase text-tcg06-dim">Throughput</span>
          <h3 class="text-[1.2rem] font-bold">Requests</h3>
        </div>
        <div style="--accent:#ff6b9d" class="absolute inset-0 flex flex-col justify-end gap-1 p-4.5 rounded-[14px] backface-hidden rotate-y-180 bg-[var(--accent)] text-tcg06-bg">
          <span class="text-[1.5rem] font-extrabold tracking-[-0.02em] tabular-nums">12k/s</span>
          <p class="text-[.78rem] font-semibold opacity-80">peak sustained</p>
        </div>
      </div>
    </div>

    <div class="group perspective-[1000px] h-40">
      <div class="relative w-full h-full transform-3d transition-transform duration-600 ease-tcg06 group-hover:rotate-y-180 group-focus-within:rotate-y-180 motion-reduce:transition-none motion-reduce:group-hover:rotate-y-0">
        <div class="absolute inset-0 flex flex-col justify-end gap-1.5 p-4.5 rounded-[14px] backface-hidden border border-white/10 bg-tcg06-face">
          <span class="text-[.7rem] font-semibold tracking-[0.1em] uppercase text-tcg06-dim">Storage</span>
          <h3 class="text-[1.2rem] font-bold">Database</h3>
        </div>
        <div style="--accent:#ffb454" class="absolute inset-0 flex flex-col justify-end gap-1 p-4.5 rounded-[14px] backface-hidden rotate-y-180 bg-[var(--accent)] text-tcg06-bg">
          <span class="text-[1.5rem] font-extrabold tracking-[-0.02em] tabular-nums">4.2TB</span>
          <p class="text-[.78rem] font-semibold opacity-80">across all shards</p>
        </div>
      </div>
    </div>

  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tcg06-bg: #0d0d12;
  --color-tcg06-bg: oklch(0.15 0.006 285);
  --color-tcg06-face: #16161d;
  --color-tcg06-face: oklch(0.2 0.008 285);
  --color-tcg06-dim: #a1a1b0;
  --color-tcg06-dim: oklch(0.72 0.01 285);
  --font-tcg06-sans: 'Space Grotesk', system-ui, sans-serif;
  --ease-tcg06: cubic-bezier(0.4, 0.2, 0.2, 1);
}
/* Root rule: the full-bleed contract. */

.tcg-06 {
  width: 100%;
  min-height: 100svh;
}

.tcg-06 ::selection {
  background: #7c6af7;
  color: #fff;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tcg06-bg: #0d0d12;
  --color-tcg06-bg: oklch(0.15 0.006 285);
  --color-tcg06-face: #16161d;
  --color-tcg06-face: oklch(0.2 0.008 285);
  --color-tcg06-dim: #a1a1b0;
  --color-tcg06-dim: oklch(0.72 0.01 285);
  --font-tcg06-sans: 'Space Grotesk', system-ui, sans-serif;
  --ease-tcg06: cubic-bezier(0.4, 0.2, 0.2, 1);
}
/* Root rule: the full-bleed contract. */

.tcg-06 {
  width: 100%;
  min-height: 100svh;
}

.tcg-06 ::selection {
  background: #7c6af7;
  color: #fff;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • v4 turned the whole 3D vocabulary into utilities — perspective-*, transform-3d, backface-hidden, rotate-y-* — so this demo's stylesheet holds only tokens. In v3 most of it would still have been arbitrary properties.
  • .flip:hover .inner becomes group-hover:rotate-y-180 on the inner: the transform is declared on the element that transforms, which is the port's recurring theme.
  • The idle-flipped first card was two CSS rules keyed to :first-child; it is now two utilities on that card, and moving the behaviour to a different card is a copy-paste.
  • group-focus-within: was added alongside every hover so a keyboard user reaching the card's contents sees the same flip — the CSS version had no keyboard path at all.
  • The per-face --accent stays inline: four instance colours, each read once.
  • motion-reduce: cancels the rotation instead of slowing it, and because both faces are in the DOM the metric remains reachable either way.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind 3D Hover Flip Card Grid
Source: https://codefronts.com/layouts/tailwind-card-grid-layout/tailwind-3d-hover-flip-card-grid/

A flashcard wall where hovering a tile rotates it 180&deg; to reveal an accent-coloured back face of metrics, and the grid never shifts because only a transform animates. v4 ships perspective-*, transform-3d, backface-hidden and rotate-y-* as real utilities, so the entire 3D scene is expressible without touching the stylesheet.
## HTML
```html
<section class="tcg-06 w-full min-h-svh bg-tcg06-bg font-tcg06-sans text-white px-6 py-12" aria-label="3D hover flip card grid demo">
  <div class="max-w-[780px] mx-auto grid [grid-template-columns:repeat(auto-fit,minmax(160px,1fr))] gap-5">

    <div class="group perspective-[1000px] h-40">
      <div class="relative w-full h-full transform-3d transition-transform duration-600 ease-tcg06 rotate-y-180 group-hover:rotate-y-0 group-focus-within:rotate-y-0 motion-reduce:transition-none motion-reduce:rotate-y-0">
        <div class="absolute inset-0 flex flex-col justify-end gap-1.5 p-4.5 rounded-[14px] backface-hidden border border-white/10 bg-tcg06-face">
          <span class="text-[.7rem] font-semibold tracking-[0.1em] uppercase text-tcg06-dim">Latency</span>
          <h3 class="text-[1.2rem] font-bold">API</h3>
        </div>
        <div style="--accent:#7c6af7" class="absolute inset-0 flex flex-col justify-end gap-1 p-4.5 rounded-[14px] backface-hidden rotate-y-180 bg-[var(--accent)] text-tcg06-bg">
          <span class="text-[1.5rem] font-extrabold tracking-[-0.02em] tabular-nums">42ms</span>
          <p class="text-[.78rem] font-semibold opacity-80">p95 response time</p>
        </div>
      </div>
    </div>

    <div class="group perspective-[1000px] h-40">
      <div class="relative w-full h-full transform-3d transition-transform duration-600 ease-tcg06 group-hover:rotate-y-180 group-focus-within:rotate-y-180 motion-reduce:transition-none motion-reduce:group-hover:rotate-y-0">
        <div class="absolute inset-0 flex flex-col justify-end gap-1.5 p-4.5 rounded-[14px] backface-hidden border border-white/10 bg-tcg06-face">
          <span class="text-[.7rem] font-semibold tracking-[0.1em] uppercase text-tcg06-dim">Uptime</span>
          <h3 class="text-[1.2rem] font-bold">SLA</h3>
        </div>
        <div style="--accent:#00c2a8" class="absolute inset-0 flex flex-col justify-end gap-1 p-4.5 rounded-[14px] backface-hidden rotate-y-180 bg-[var(--accent)] text-tcg06-bg">
          <span class="text-[1.5rem] font-extrabold tracking-[-0.02em] tabular-nums">99.98%</span>
          <p class="text-[.78rem] font-semibold opacity-80">last 90 days</p>
        </div>
      </div>
    </div>

    <div class="group perspective-[1000px] h-40">
      <div class="relative w-full h-full transform-3d transition-transform duration-600 ease-tcg06 group-hover:rotate-y-180 group-focus-within:rotate-y-180 motion-reduce:transition-none motion-reduce:group-hover:rotate-y-0">
        <div class="absolute inset-0 flex flex-col justify-end gap-1.5 p-4.5 rounded-[14px] backface-hidden border border-white/10 bg-tcg06-face">
          <span class="text-[.7rem] font-semibold tracking-[0.1em] uppercase text-tcg06-dim">Throughput</span>
          <h3 class="text-[1.2rem] font-bold">Requests</h3>
        </div>
        <div style="--accent:#ff6b9d" class="absolute inset-0 flex flex-col justify-end gap-1 p-4.5 rounded-[14px] backface-hidden rotate-y-180 bg-[var(--accent)] text-tcg06-bg">
          <span class="text-[1.5rem] font-extrabold tracking-[-0.02em] tabular-nums">12k/s</span>
          <p class="text-[.78rem] font-semibold opacity-80">peak sustained</p>
        </div>
      </div>
    </div>

    <div class="group perspective-[1000px] h-40">
      <div class="relative w-full h-full transform-3d transition-transform duration-600 ease-tcg06 group-hover:rotate-y-180 group-focus-within:rotate-y-180 motion-reduce:transition-none motion-reduce:group-hover:rotate-y-0">
        <div class="absolute inset-0 flex flex-col justify-end gap-1.5 p-4.5 rounded-[14px] backface-hidden border border-white/10 bg-tcg06-face">
          <span class="text-[.7rem] font-semibold tracking-[0.1em] uppercase text-tcg06-dim">Storage</span>
          <h3 class="text-[1.2rem] font-bold">Database</h3>
        </div>
        <div style="--accent:#ffb454" class="absolute inset-0 flex flex-col justify-end gap-1 p-4.5 rounded-[14px] backface-hidden rotate-y-180 bg-[var(--accent)] text-tcg06-bg">
          <span class="text-[1.5rem] font-extrabold tracking-[-0.02em] tabular-nums">4.2TB</span>
          <p class="text-[.78rem] font-semibold opacity-80">across all shards</p>
        </div>
      </div>
    </div>

  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tcg06-bg: #0d0d12;
  --color-tcg06-bg: oklch(0.15 0.006 285);
  --color-tcg06-face: #16161d;
  --color-tcg06-face: oklch(0.2 0.008 285);
  --color-tcg06-dim: #a1a1b0;
  --color-tcg06-dim: oklch(0.72 0.01 285);
  --font-tcg06-sans: 'Space Grotesk', system-ui, sans-serif;
  --ease-tcg06: cubic-bezier(0.4, 0.2, 0.2, 1);
}
/* Root rule: the full-bleed contract. */

.tcg-06 {
  width: 100%;
  min-height: 100svh;
}

.tcg-06 ::selection {
  background: #7c6af7;
  color: #fff;
}
```

How this works

Three layers, all in utilities. The wrapper owns the scene, the inner owns the rotation, the faces own their hiding:

<div class="group perspective-[1000px] h-40">
  <div class="relative w-full h-full transform-3d transition-transform duration-600
              group-hover:rotate-y-180">
    <div class="absolute inset-0 backface-hidden …">front</div>
    <div class="absolute inset-0 backface-hidden rotate-y-180 …">back</div>

perspective must be on the wrapper, not the rotating element — on the inner it flattens the flip into a squash. That constraint is unchanged from CSS; what changes is that the mistake is now visible in the class list rather than buried in a rule.

The hover moved to the group. The CSS version wrote .flip:hover .inner { transform: rotateY(180deg) }. Here the rotation is declared on the element that rotates, keyed to the wrapper's hover. One rule becomes one utility, on the right element.

The idle-flipped tile is two utilities. The first card rests on its back face so the pattern is legible without hovering: rotate-y-180 group-hover:rotate-y-0. The CSS needed a :first-child rule plus a :first-child:hover rule; the tile now simply says what it does.

Hover-only is not enough on touch. Both faces stay in the DOM, so the metric is always available to a screen reader, and the note in the gotchas is the same one the CSS version carried: a production build wants a tap-to-flip state, which in v4 is data-flipped plus group-data-flipped:rotate-y-180.

Make it yours

  • Flip on the X axis by swapping every rotate-y-* for rotate-x-* on the inner and the back face together.
  • Change the tempo with duration-* and an ease-* token; clip-free rotation tolerates overshoot curves well.
  • Recolour any back face through its inline --accent.
  • Convert to tap-to-flip: put data-flipped on the wrapper from a five-line controller and add group-data-flipped:rotate-y-180 beside the hover variant.
  • Add a pre-flip lift with group-hover:-translate-y-0.5 on the wrapper for a two-stage micro-interaction.

Gotchas — read before shipping

  • perspective-* belongs on the wrapper. On the rotating element itself the 3D depth collapses and you get a flat squash.
  • Both faces need backface-hidden. Omit it on either and the mirrored reverse of the other face shows through as garbled text mid-flip.
  • The wrapper needs an explicit height (h-40): the faces are absolutely positioned, so without it the tile has no height at all.
  • Hover-only flips are unavailable on touch devices. Keep both faces in the DOM (they are) and add a tap state for production.
  • Safari benefits from the -webkit- backface prefix, which Tailwind emits for backface-hidden automatically — one fewer thing to hand-write than in the CSS version.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

3D transforms are broadly supported and run far back; v4's transform-3d, perspective-* and rotate-y-* utilities require Tailwind v4 itself, hence the floor quoted.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Search CodeFronts

Loading…