17 / 23
All 23 in 23 Tailwind CSS Flip Cards
- 01Tailwind Product Comparison Flip Card with Hover, Tap & Keyboard Reveal
- 02Tailwind Pricing Plan Flip Card with Gradient Price & Feature Reveal
- 03Tailwind Gift Card Flip with Copy-to-Clipboard Coupon Code Reveal
- 04Tailwind Meet the Team Flip Card with Bio, Socials & Contact Reveal
- 05Tailwind Portfolio Flip Card with Animated Skill Bar Proficiency Meters
- 06Tailwind Language Flashcard Flip with Phonetics, Example & Memory Tip
- 07Tailwind Trivia Quiz Flip Card with Answer Reveal & Score Readout
- 08Tailwind Gaming Trading Card Flip with Character Stats & Rarity Glow
- 09Tailwind Photo Gallery Flip Card with EXIF Metadata & Download CTA
- 10Tailwind Video Thumbnail Flip Card with Playlist, Stats & Watch CTA
- 11Tailwind Testimonial Flip Card with Star Rating & Reviewer Profile
- 12Tailwind Digital Business Card Flip with vCard Contacts & QR Panel
- 13Tailwind Restaurant Menu Card with Sliding Ingredient & Nutrition Reveal
- 14Tailwind Process Steps Flip Card Grid with Task Lists & Timelines
- 15Tailwind Vertical Axis Flip Card with rotateX Top-to-Bottom Reveal
- 16Tailwind Click-to-Flip Card with Persisted State & Keyboard Control
- 17Tailwind Auto-Height Flip Card with CSS Grid Stacking & Cross-Fade
- 18Tailwind Responsive Flip Card Grid with Per-Card 3D Perspective
- 19Tailwind Luxury Product Card with Hinged Book-Open 3D Unfold
- 20Tailwind Travel Postcard Flip Card with Diagonal rotate3d Twist
- 21Tailwind Album Card with clip-path Iris Aperture Reveal (No 3D)
- 22Tailwind Event Ticket Card with Bi-Fold Stub Reveal & QR Panel
- 23Tailwind Scratch Card with Diagonal clip-path Peel & Prize Reveal
Tailwind Auto-Height Flip Card with CSS Grid Stacking & Cross-Fade
The CMS-safe variant: no pixel heights anywhere. Both faces live in the same grid cell via col-start-1 row-start-1, so the taller one sets the card's height and content of any length fits. The trade is real — this is a cross-fade with a rotation flourish, not a true 3D turn — and the port fixes the technique's accessibility hole by toggling aria-hidden on the face that is not showing.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tfc-17 w-full min-h-svh grid place-items-center content-center gap-6 bg-tfc17-bg font-tfc17-sans px-5 py-10 text-tfc17-ink">
<div class="text-center">
<h2 class="text-base font-bold text-white">Auto-height flip cards</h2>
<p class="mt-1 text-[11px] text-tfc17-muted">No fixed heights — each card grows to its tallest face</p>
<code class="mt-2 inline-block rounded border border-tfc17-violet/25 bg-tfc17-violet/15 px-2 py-0.5 font-tfc17-mono text-[11px] text-tfc17-violet">grid + col-start-1 row-start-1</code>
</div>
<div class="flex flex-wrap justify-center gap-5">
<div class="group relative grid w-[280px] rounded-2xl has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-tfc17-violet has-[:focus-visible]:outline-offset-4">
<input id="tfc-17-a" type="checkbox" class="peer sr-only">
<label for="tfc-17-a" class="absolute inset-0 z-20 cursor-pointer rounded-2xl"><span class="sr-only">Show the extended detail for card A</span></label>
<div class="col-start-1 row-start-1 rounded-2xl border border-tfc17-violet/20 bg-linear-145 from-tfc17-a1 to-tfc17-a2 p-6 transition-[opacity,transform] duration-500 ease-[cubic-bezier(.4,0,.2,1)] group-hover:opacity-0 group-hover:[transform:rotateY(90deg)] group-has-checked:opacity-0 group-has-checked:[transform:rotateY(90deg)] group-has-[:focus-visible]:opacity-0 group-has-[:focus-visible]:[transform:rotateY(90deg)] motion-reduce:transition-none">
<p class="mb-2.5 text-[9px] font-bold uppercase tracking-[0.15em] text-tfc17-violet">Dynamic card A</p>
<h3 class="mb-2 text-lg font-extrabold text-white">Short front</h3>
<p class="text-[13px] leading-[1.6] text-tfc17-muted">This front face has minimal content. The back has much more, and the card simply grows.</p>
<p class="mt-3.5 text-[10px] tracking-[0.08em] text-tfc17-dim">Hover, tap or press Space to expand →</p>
</div>
<div aria-hidden="true" class="col-start-1 row-start-1 rounded-2xl border border-tfc17-sky/20 bg-linear-145 from-tfc17-b1 to-tfc17-b2 p-6 opacity-0 [transform:rotateY(-90deg)] transition-[opacity,transform] duration-500 ease-[cubic-bezier(.4,0,.2,1)] group-hover:opacity-100 group-hover:[transform:rotateY(0deg)] group-has-checked:opacity-100 group-has-checked:[transform:rotateY(0deg)] group-has-[:focus-visible]:opacity-100 group-has-[:focus-visible]:[transform:rotateY(0deg)] motion-reduce:transition-none">
<p class="mb-2.5 text-[9px] font-bold uppercase tracking-[0.15em] text-tfc17-sky">Full detail · back</p>
<h3 class="mb-2.5 text-base font-extrabold text-white">Extended back content</h3>
<p class="text-[13px] leading-[1.7] text-tfc17-ink/70">The grid technique means this face can hold as much content as it needs and the card height adapts — no hardcoded pixels anywhere.</p>
<ul class="mt-2.5 flex flex-col gap-1.5 text-xs text-tfc17-ink/65">
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-sky">Both faces share one grid cell</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-sky">Opacity plus rotateY sells the turn</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-sky">The taller face sets the height</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-sky">Zero JavaScript</li>
</ul>
</div>
</div>
<div class="group relative grid w-[280px] rounded-2xl has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-tfc17-pink has-[:focus-visible]:outline-offset-4">
<input id="tfc-17-b" type="checkbox" class="peer sr-only">
<label for="tfc-17-b" class="absolute inset-0 z-20 cursor-pointer rounded-2xl"><span class="sr-only">Show the summary for card B</span></label>
<div class="col-start-1 row-start-1 rounded-2xl border border-tfc17-pink/20 bg-linear-145 from-tfc17-c1 to-tfc17-c2 p-6 transition-[opacity,transform] duration-500 ease-[cubic-bezier(.4,0,.2,1)] group-hover:opacity-0 group-hover:[transform:rotateY(90deg)] group-has-checked:opacity-0 group-has-checked:[transform:rotateY(90deg)] group-has-[:focus-visible]:opacity-0 group-has-[:focus-visible]:[transform:rotateY(90deg)] motion-reduce:transition-none">
<p class="mb-2.5 text-[9px] font-bold uppercase tracking-[0.15em] text-tfc17-pink">Dynamic card B</p>
<h3 class="mb-2 text-lg font-extrabold text-white">Longer front content</h3>
<p class="text-[13px] leading-[1.6] text-tfc17-muted">This card carries far more on the front — a paragraph, a list, and a closing note — and still no height is declared anywhere.</p>
<ul class="mt-3 flex flex-col gap-1.5 text-xs text-tfc17-ink/65">
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-pink">Point one with some detail</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-pink">Point two with some detail</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-pink">Point three with some detail</li>
</ul>
<p class="mt-3.5 text-[10px] tracking-[0.08em] text-tfc17-dim">Hover, tap or press Space for the summary →</p>
</div>
<div aria-hidden="true" class="col-start-1 row-start-1 rounded-2xl border border-tfc17-green/20 bg-linear-145 from-tfc17-d1 to-tfc17-d2 p-6 opacity-0 [transform:rotateY(-90deg)] transition-[opacity,transform] duration-500 ease-[cubic-bezier(.4,0,.2,1)] group-hover:opacity-100 group-hover:[transform:rotateY(0deg)] group-has-checked:opacity-100 group-has-checked:[transform:rotateY(0deg)] group-has-[:focus-visible]:opacity-100 group-has-[:focus-visible]:[transform:rotateY(0deg)] motion-reduce:transition-none">
<p class="mb-2.5 text-[9px] font-bold uppercase tracking-[0.15em] text-tfc17-green">TL;DR · back</p>
<h3 class="mb-2.5 text-base font-extrabold text-white">Short summary</h3>
<p class="text-[13px] leading-[1.7] text-tfc17-ink/70">Each face owns its natural height. No JS, no fixed pixels, no clipped content.</p>
</div>
</div>
</div>
<p class="max-w-[580px] rounded-xl border border-tfc17-violet/15 bg-tfc17-violet/6 px-5 py-4 text-center text-xs leading-[1.7] text-tfc17-muted text-pretty">Both faces sit on <code class="rounded bg-tfc17-violet/12 px-1 font-tfc17-mono text-tfc17-violet">col-start-1 row-start-1</code>, so they overlap in one grid cell and the tallest one sets the card height. Opacity fades while <code class="rounded bg-tfc17-violet/12 px-1 font-tfc17-mono text-tfc17-violet">rotateY(±90deg)</code> adds depth — an auto-height flip with no JavaScript.</p>
</section><section class="tfc-17 w-full min-h-svh grid place-items-center content-center gap-6 bg-tfc17-bg font-tfc17-sans px-5 py-10 text-tfc17-ink">
<div class="text-center">
<h2 class="text-base font-bold text-white">Auto-height flip cards</h2>
<p class="mt-1 text-[11px] text-tfc17-muted">No fixed heights — each card grows to its tallest face</p>
<code class="mt-2 inline-block rounded border border-tfc17-violet/25 bg-tfc17-violet/15 px-2 py-0.5 font-tfc17-mono text-[11px] text-tfc17-violet">grid + col-start-1 row-start-1</code>
</div>
<div class="flex flex-wrap justify-center gap-5">
<div class="group relative grid w-[280px] rounded-2xl has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-tfc17-violet has-[:focus-visible]:outline-offset-4">
<input id="tfc-17-a" type="checkbox" class="peer sr-only">
<label for="tfc-17-a" class="absolute inset-0 z-20 cursor-pointer rounded-2xl"><span class="sr-only">Show the extended detail for card A</span></label>
<div class="col-start-1 row-start-1 rounded-2xl border border-tfc17-violet/20 bg-linear-145 from-tfc17-a1 to-tfc17-a2 p-6 transition-[opacity,transform] duration-500 ease-[cubic-bezier(.4,0,.2,1)] group-hover:opacity-0 group-hover:[transform:rotateY(90deg)] group-has-checked:opacity-0 group-has-checked:[transform:rotateY(90deg)] group-has-[:focus-visible]:opacity-0 group-has-[:focus-visible]:[transform:rotateY(90deg)] motion-reduce:transition-none">
<p class="mb-2.5 text-[9px] font-bold uppercase tracking-[0.15em] text-tfc17-violet">Dynamic card A</p>
<h3 class="mb-2 text-lg font-extrabold text-white">Short front</h3>
<p class="text-[13px] leading-[1.6] text-tfc17-muted">This front face has minimal content. The back has much more, and the card simply grows.</p>
<p class="mt-3.5 text-[10px] tracking-[0.08em] text-tfc17-dim">Hover, tap or press Space to expand →</p>
</div>
<div aria-hidden="true" class="col-start-1 row-start-1 rounded-2xl border border-tfc17-sky/20 bg-linear-145 from-tfc17-b1 to-tfc17-b2 p-6 opacity-0 [transform:rotateY(-90deg)] transition-[opacity,transform] duration-500 ease-[cubic-bezier(.4,0,.2,1)] group-hover:opacity-100 group-hover:[transform:rotateY(0deg)] group-has-checked:opacity-100 group-has-checked:[transform:rotateY(0deg)] group-has-[:focus-visible]:opacity-100 group-has-[:focus-visible]:[transform:rotateY(0deg)] motion-reduce:transition-none">
<p class="mb-2.5 text-[9px] font-bold uppercase tracking-[0.15em] text-tfc17-sky">Full detail · back</p>
<h3 class="mb-2.5 text-base font-extrabold text-white">Extended back content</h3>
<p class="text-[13px] leading-[1.7] text-tfc17-ink/70">The grid technique means this face can hold as much content as it needs and the card height adapts — no hardcoded pixels anywhere.</p>
<ul class="mt-2.5 flex flex-col gap-1.5 text-xs text-tfc17-ink/65">
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-sky">Both faces share one grid cell</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-sky">Opacity plus rotateY sells the turn</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-sky">The taller face sets the height</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-sky">Zero JavaScript</li>
</ul>
</div>
</div>
<div class="group relative grid w-[280px] rounded-2xl has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-tfc17-pink has-[:focus-visible]:outline-offset-4">
<input id="tfc-17-b" type="checkbox" class="peer sr-only">
<label for="tfc-17-b" class="absolute inset-0 z-20 cursor-pointer rounded-2xl"><span class="sr-only">Show the summary for card B</span></label>
<div class="col-start-1 row-start-1 rounded-2xl border border-tfc17-pink/20 bg-linear-145 from-tfc17-c1 to-tfc17-c2 p-6 transition-[opacity,transform] duration-500 ease-[cubic-bezier(.4,0,.2,1)] group-hover:opacity-0 group-hover:[transform:rotateY(90deg)] group-has-checked:opacity-0 group-has-checked:[transform:rotateY(90deg)] group-has-[:focus-visible]:opacity-0 group-has-[:focus-visible]:[transform:rotateY(90deg)] motion-reduce:transition-none">
<p class="mb-2.5 text-[9px] font-bold uppercase tracking-[0.15em] text-tfc17-pink">Dynamic card B</p>
<h3 class="mb-2 text-lg font-extrabold text-white">Longer front content</h3>
<p class="text-[13px] leading-[1.6] text-tfc17-muted">This card carries far more on the front — a paragraph, a list, and a closing note — and still no height is declared anywhere.</p>
<ul class="mt-3 flex flex-col gap-1.5 text-xs text-tfc17-ink/65">
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-pink">Point one with some detail</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-pink">Point two with some detail</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-pink">Point three with some detail</li>
</ul>
<p class="mt-3.5 text-[10px] tracking-[0.08em] text-tfc17-dim">Hover, tap or press Space for the summary →</p>
</div>
<div aria-hidden="true" class="col-start-1 row-start-1 rounded-2xl border border-tfc17-green/20 bg-linear-145 from-tfc17-d1 to-tfc17-d2 p-6 opacity-0 [transform:rotateY(-90deg)] transition-[opacity,transform] duration-500 ease-[cubic-bezier(.4,0,.2,1)] group-hover:opacity-100 group-hover:[transform:rotateY(0deg)] group-has-checked:opacity-100 group-has-checked:[transform:rotateY(0deg)] group-has-[:focus-visible]:opacity-100 group-has-[:focus-visible]:[transform:rotateY(0deg)] motion-reduce:transition-none">
<p class="mb-2.5 text-[9px] font-bold uppercase tracking-[0.15em] text-tfc17-green">TL;DR · back</p>
<h3 class="mb-2.5 text-base font-extrabold text-white">Short summary</h3>
<p class="text-[13px] leading-[1.7] text-tfc17-ink/70">Each face owns its natural height. No JS, no fixed pixels, no clipped content.</p>
</div>
</div>
</div>
<p class="max-w-[580px] rounded-xl border border-tfc17-violet/15 bg-tfc17-violet/6 px-5 py-4 text-center text-xs leading-[1.7] text-tfc17-muted text-pretty">Both faces sit on <code class="rounded bg-tfc17-violet/12 px-1 font-tfc17-mono text-tfc17-violet">col-start-1 row-start-1</code>, so they overlap in one grid cell and the tallest one sets the card height. Opacity fades while <code class="rounded bg-tfc17-violet/12 px-1 font-tfc17-mono text-tfc17-violet">rotateY(±90deg)</code> adds depth — an auto-height flip with no JavaScript.</p>
</section>@import "tailwindcss";
@theme {
--color-tfc17-bg: #08060e;
--color-tfc17-bg: oklch(0.13 0.02 295);
--color-tfc17-a1: #130e28;
--color-tfc17-a1: oklch(0.19 0.05 295);
--color-tfc17-a2: #0a0718;
--color-tfc17-a2: oklch(0.14 0.035 295);
--color-tfc17-b1: #0e1a28;
--color-tfc17-b1: oklch(0.2 0.04 245);
--color-tfc17-b2: #080f18;
--color-tfc17-b2: oklch(0.15 0.025 245);
--color-tfc17-c1: #1a0e18;
--color-tfc17-c1: oklch(0.19 0.04 330);
--color-tfc17-c2: #0f0810;
--color-tfc17-c2: oklch(0.14 0.03 330);
--color-tfc17-d1: #0a1a0e;
--color-tfc17-d1: oklch(0.19 0.045 150);
--color-tfc17-d2: #060e08;
--color-tfc17-d2: oklch(0.13 0.025 150);
--color-tfc17-violet: #8b5cf6;
--color-tfc17-violet: oklch(0.63 0.2 295);
--color-tfc17-sky: #38bdf8;
--color-tfc17-sky: oklch(0.76 0.13 230);
--color-tfc17-pink: #f472b6;
--color-tfc17-pink: oklch(0.73 0.17 350);
--color-tfc17-green: #4ade80;
--color-tfc17-green: oklch(0.8 0.17 152);
--color-tfc17-ink: #faf5ff;
--color-tfc17-ink: oklch(0.98 0.015 300);
--color-tfc17-muted: #b3a8c2;
--color-tfc17-muted: oklch(0.76 0.035 300);
--color-tfc17-dim: #87809a;
--color-tfc17-dim: oklch(0.62 0.03 300);
--font-tfc17-sans: 'Sora', system-ui, sans-serif;
--font-tfc17-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tfc-17 {
width: 100%;
min-height: 100svh;
}
@media (prefers-reduced-motion: reduce) {
.tfc-17 * {
transition-duration: 1ms !important;
}
}@import "tailwindcss";
@theme {
--color-tfc17-bg: #08060e;
--color-tfc17-bg: oklch(0.13 0.02 295);
--color-tfc17-a1: #130e28;
--color-tfc17-a1: oklch(0.19 0.05 295);
--color-tfc17-a2: #0a0718;
--color-tfc17-a2: oklch(0.14 0.035 295);
--color-tfc17-b1: #0e1a28;
--color-tfc17-b1: oklch(0.2 0.04 245);
--color-tfc17-b2: #080f18;
--color-tfc17-b2: oklch(0.15 0.025 245);
--color-tfc17-c1: #1a0e18;
--color-tfc17-c1: oklch(0.19 0.04 330);
--color-tfc17-c2: #0f0810;
--color-tfc17-c2: oklch(0.14 0.03 330);
--color-tfc17-d1: #0a1a0e;
--color-tfc17-d1: oklch(0.19 0.045 150);
--color-tfc17-d2: #060e08;
--color-tfc17-d2: oklch(0.13 0.025 150);
--color-tfc17-violet: #8b5cf6;
--color-tfc17-violet: oklch(0.63 0.2 295);
--color-tfc17-sky: #38bdf8;
--color-tfc17-sky: oklch(0.76 0.13 230);
--color-tfc17-pink: #f472b6;
--color-tfc17-pink: oklch(0.73 0.17 350);
--color-tfc17-green: #4ade80;
--color-tfc17-green: oklch(0.8 0.17 152);
--color-tfc17-ink: #faf5ff;
--color-tfc17-ink: oklch(0.98 0.015 300);
--color-tfc17-muted: #b3a8c2;
--color-tfc17-muted: oklch(0.76 0.035 300);
--color-tfc17-dim: #87809a;
--color-tfc17-dim: oklch(0.62 0.03 300);
--font-tfc17-sans: 'Sora', system-ui, sans-serif;
--font-tfc17-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tfc-17 {
width: 100%;
min-height: 100svh;
}
@media (prefers-reduced-motion: reduce) {
.tfc-17 * {
transition-duration: 1ms !important;
}
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<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.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tfc17-bg: #08060e;
--color-tfc17-bg: oklch(0.13 0.02 295);
--color-tfc17-a1: #130e28;
--color-tfc17-a1: oklch(0.19 0.05 295);
--color-tfc17-a2: #0a0718;
--color-tfc17-a2: oklch(0.14 0.035 295);
--color-tfc17-b1: #0e1a28;
--color-tfc17-b1: oklch(0.2 0.04 245);
--color-tfc17-b2: #080f18;
--color-tfc17-b2: oklch(0.15 0.025 245);
--color-tfc17-c1: #1a0e18;
--color-tfc17-c1: oklch(0.19 0.04 330);
--color-tfc17-c2: #0f0810;
--color-tfc17-c2: oklch(0.14 0.03 330);
--color-tfc17-d1: #0a1a0e;
--color-tfc17-d1: oklch(0.19 0.045 150);
--color-tfc17-d2: #060e08;
--color-tfc17-d2: oklch(0.13 0.025 150);
--color-tfc17-violet: #8b5cf6;
--color-tfc17-violet: oklch(0.63 0.2 295);
--color-tfc17-sky: #38bdf8;
--color-tfc17-sky: oklch(0.76 0.13 230);
--color-tfc17-pink: #f472b6;
--color-tfc17-pink: oklch(0.73 0.17 350);
--color-tfc17-green: #4ade80;
--color-tfc17-green: oklch(0.8 0.17 152);
--color-tfc17-ink: #faf5ff;
--color-tfc17-ink: oklch(0.98 0.015 300);
--color-tfc17-muted: #b3a8c2;
--color-tfc17-muted: oklch(0.76 0.035 300);
--color-tfc17-dim: #87809a;
--color-tfc17-dim: oklch(0.62 0.03 300);
--font-tfc17-sans: 'Sora', system-ui, sans-serif;
--font-tfc17-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tfc-17 {
width: 100%;
min-height: 100svh;
}
@media (prefers-reduced-motion: reduce) {
.tfc-17 * {
transition-duration: 1ms !important;
}
}@import "tailwindcss";
@theme {
--color-tfc17-bg: #08060e;
--color-tfc17-bg: oklch(0.13 0.02 295);
--color-tfc17-a1: #130e28;
--color-tfc17-a1: oklch(0.19 0.05 295);
--color-tfc17-a2: #0a0718;
--color-tfc17-a2: oklch(0.14 0.035 295);
--color-tfc17-b1: #0e1a28;
--color-tfc17-b1: oklch(0.2 0.04 245);
--color-tfc17-b2: #080f18;
--color-tfc17-b2: oklch(0.15 0.025 245);
--color-tfc17-c1: #1a0e18;
--color-tfc17-c1: oklch(0.19 0.04 330);
--color-tfc17-c2: #0f0810;
--color-tfc17-c2: oklch(0.14 0.03 330);
--color-tfc17-d1: #0a1a0e;
--color-tfc17-d1: oklch(0.19 0.045 150);
--color-tfc17-d2: #060e08;
--color-tfc17-d2: oklch(0.13 0.025 150);
--color-tfc17-violet: #8b5cf6;
--color-tfc17-violet: oklch(0.63 0.2 295);
--color-tfc17-sky: #38bdf8;
--color-tfc17-sky: oklch(0.76 0.13 230);
--color-tfc17-pink: #f472b6;
--color-tfc17-pink: oklch(0.73 0.17 350);
--color-tfc17-green: #4ade80;
--color-tfc17-green: oklch(0.8 0.17 152);
--color-tfc17-ink: #faf5ff;
--color-tfc17-ink: oklch(0.98 0.015 300);
--color-tfc17-muted: #b3a8c2;
--color-tfc17-muted: oklch(0.76 0.035 300);
--color-tfc17-dim: #87809a;
--color-tfc17-dim: oklch(0.62 0.03 300);
--font-tfc17-sans: 'Sora', system-ui, sans-serif;
--font-tfc17-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tfc-17 {
width: 100%;
min-height: 100svh;
}
@media (prefers-reduced-motion: reduce) {
.tfc-17 * {
transition-duration: 1ms !important;
}
}<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=Sora:wght@400;600;700;800&family=JetBrains+Mono:wght@400&display=swap">
<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=Sora:wght@400;600;700;800&family=JetBrains+Mono:wght@400&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
grid-area: 1/1becamecol-start-1 row-start-1, which is longer and clearer: the two axes are stated separately, so nobody has to remember the four-value shorthand order.- The two nth-child palette overrides became per-card tokens, same as demo 14 — the second card is no longer defined by being second.
- UI change from the CSS original: the hidden face is
aria-hidden(the original's own gotcha list flagged this and left it unfixed), both cards are keyboard-reachable, and the technique note is a real paragraph rather than a styled div.
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 Auto-Height Flip Card with CSS Grid Stacking & Cross-Fade
Source: https://codefronts.com/components/tailwind-css-flip-cards/tailwind-auto-height-flip-card-grid-stacking-cross-fade/
The CMS-safe variant: no pixel heights anywhere. Both faces live in the same grid cell via col-start-1 row-start-1, so the taller one sets the card's height and content of any length fits. The trade is real — this is a cross-fade with a rotation flourish, not a true 3D turn — and the port fixes the technique's accessibility hole by toggling aria-hidden on the face that is not showing.
## HTML
```html
<section class="tfc-17 w-full min-h-svh grid place-items-center content-center gap-6 bg-tfc17-bg font-tfc17-sans px-5 py-10 text-tfc17-ink">
<div class="text-center">
<h2 class="text-base font-bold text-white">Auto-height flip cards</h2>
<p class="mt-1 text-[11px] text-tfc17-muted">No fixed heights — each card grows to its tallest face</p>
<code class="mt-2 inline-block rounded border border-tfc17-violet/25 bg-tfc17-violet/15 px-2 py-0.5 font-tfc17-mono text-[11px] text-tfc17-violet">grid + col-start-1 row-start-1</code>
</div>
<div class="flex flex-wrap justify-center gap-5">
<div class="group relative grid w-[280px] rounded-2xl has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-tfc17-violet has-[:focus-visible]:outline-offset-4">
<input id="tfc-17-a" type="checkbox" class="peer sr-only">
<label for="tfc-17-a" class="absolute inset-0 z-20 cursor-pointer rounded-2xl"><span class="sr-only">Show the extended detail for card A</span></label>
<div class="col-start-1 row-start-1 rounded-2xl border border-tfc17-violet/20 bg-linear-145 from-tfc17-a1 to-tfc17-a2 p-6 transition-[opacity,transform] duration-500 ease-[cubic-bezier(.4,0,.2,1)] group-hover:opacity-0 group-hover:[transform:rotateY(90deg)] group-has-checked:opacity-0 group-has-checked:[transform:rotateY(90deg)] group-has-[:focus-visible]:opacity-0 group-has-[:focus-visible]:[transform:rotateY(90deg)] motion-reduce:transition-none">
<p class="mb-2.5 text-[9px] font-bold uppercase tracking-[0.15em] text-tfc17-violet">Dynamic card A</p>
<h3 class="mb-2 text-lg font-extrabold text-white">Short front</h3>
<p class="text-[13px] leading-[1.6] text-tfc17-muted">This front face has minimal content. The back has much more, and the card simply grows.</p>
<p class="mt-3.5 text-[10px] tracking-[0.08em] text-tfc17-dim">Hover, tap or press Space to expand →</p>
</div>
<div aria-hidden="true" class="col-start-1 row-start-1 rounded-2xl border border-tfc17-sky/20 bg-linear-145 from-tfc17-b1 to-tfc17-b2 p-6 opacity-0 [transform:rotateY(-90deg)] transition-[opacity,transform] duration-500 ease-[cubic-bezier(.4,0,.2,1)] group-hover:opacity-100 group-hover:[transform:rotateY(0deg)] group-has-checked:opacity-100 group-has-checked:[transform:rotateY(0deg)] group-has-[:focus-visible]:opacity-100 group-has-[:focus-visible]:[transform:rotateY(0deg)] motion-reduce:transition-none">
<p class="mb-2.5 text-[9px] font-bold uppercase tracking-[0.15em] text-tfc17-sky">Full detail · back</p>
<h3 class="mb-2.5 text-base font-extrabold text-white">Extended back content</h3>
<p class="text-[13px] leading-[1.7] text-tfc17-ink/70">The grid technique means this face can hold as much content as it needs and the card height adapts — no hardcoded pixels anywhere.</p>
<ul class="mt-2.5 flex flex-col gap-1.5 text-xs text-tfc17-ink/65">
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-sky">Both faces share one grid cell</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-sky">Opacity plus rotateY sells the turn</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-sky">The taller face sets the height</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-sky">Zero JavaScript</li>
</ul>
</div>
</div>
<div class="group relative grid w-[280px] rounded-2xl has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-tfc17-pink has-[:focus-visible]:outline-offset-4">
<input id="tfc-17-b" type="checkbox" class="peer sr-only">
<label for="tfc-17-b" class="absolute inset-0 z-20 cursor-pointer rounded-2xl"><span class="sr-only">Show the summary for card B</span></label>
<div class="col-start-1 row-start-1 rounded-2xl border border-tfc17-pink/20 bg-linear-145 from-tfc17-c1 to-tfc17-c2 p-6 transition-[opacity,transform] duration-500 ease-[cubic-bezier(.4,0,.2,1)] group-hover:opacity-0 group-hover:[transform:rotateY(90deg)] group-has-checked:opacity-0 group-has-checked:[transform:rotateY(90deg)] group-has-[:focus-visible]:opacity-0 group-has-[:focus-visible]:[transform:rotateY(90deg)] motion-reduce:transition-none">
<p class="mb-2.5 text-[9px] font-bold uppercase tracking-[0.15em] text-tfc17-pink">Dynamic card B</p>
<h3 class="mb-2 text-lg font-extrabold text-white">Longer front content</h3>
<p class="text-[13px] leading-[1.6] text-tfc17-muted">This card carries far more on the front — a paragraph, a list, and a closing note — and still no height is declared anywhere.</p>
<ul class="mt-3 flex flex-col gap-1.5 text-xs text-tfc17-ink/65">
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-pink">Point one with some detail</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-pink">Point two with some detail</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-pink">Point three with some detail</li>
</ul>
<p class="mt-3.5 text-[10px] tracking-[0.08em] text-tfc17-dim">Hover, tap or press Space for the summary →</p>
</div>
<div aria-hidden="true" class="col-start-1 row-start-1 rounded-2xl border border-tfc17-green/20 bg-linear-145 from-tfc17-d1 to-tfc17-d2 p-6 opacity-0 [transform:rotateY(-90deg)] transition-[opacity,transform] duration-500 ease-[cubic-bezier(.4,0,.2,1)] group-hover:opacity-100 group-hover:[transform:rotateY(0deg)] group-has-checked:opacity-100 group-has-checked:[transform:rotateY(0deg)] group-has-[:focus-visible]:opacity-100 group-has-[:focus-visible]:[transform:rotateY(0deg)] motion-reduce:transition-none">
<p class="mb-2.5 text-[9px] font-bold uppercase tracking-[0.15em] text-tfc17-green">TL;DR · back</p>
<h3 class="mb-2.5 text-base font-extrabold text-white">Short summary</h3>
<p class="text-[13px] leading-[1.7] text-tfc17-ink/70">Each face owns its natural height. No JS, no fixed pixels, no clipped content.</p>
</div>
</div>
</div>
<p class="max-w-[580px] rounded-xl border border-tfc17-violet/15 bg-tfc17-violet/6 px-5 py-4 text-center text-xs leading-[1.7] text-tfc17-muted text-pretty">Both faces sit on <code class="rounded bg-tfc17-violet/12 px-1 font-tfc17-mono text-tfc17-violet">col-start-1 row-start-1</code>, so they overlap in one grid cell and the tallest one sets the card height. Opacity fades while <code class="rounded bg-tfc17-violet/12 px-1 font-tfc17-mono text-tfc17-violet">rotateY(±90deg)</code> adds depth — an auto-height flip with no JavaScript.</p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tfc17-bg: #08060e;
--color-tfc17-bg: oklch(0.13 0.02 295);
--color-tfc17-a1: #130e28;
--color-tfc17-a1: oklch(0.19 0.05 295);
--color-tfc17-a2: #0a0718;
--color-tfc17-a2: oklch(0.14 0.035 295);
--color-tfc17-b1: #0e1a28;
--color-tfc17-b1: oklch(0.2 0.04 245);
--color-tfc17-b2: #080f18;
--color-tfc17-b2: oklch(0.15 0.025 245);
--color-tfc17-c1: #1a0e18;
--color-tfc17-c1: oklch(0.19 0.04 330);
--color-tfc17-c2: #0f0810;
--color-tfc17-c2: oklch(0.14 0.03 330);
--color-tfc17-d1: #0a1a0e;
--color-tfc17-d1: oklch(0.19 0.045 150);
--color-tfc17-d2: #060e08;
--color-tfc17-d2: oklch(0.13 0.025 150);
--color-tfc17-violet: #8b5cf6;
--color-tfc17-violet: oklch(0.63 0.2 295);
--color-tfc17-sky: #38bdf8;
--color-tfc17-sky: oklch(0.76 0.13 230);
--color-tfc17-pink: #f472b6;
--color-tfc17-pink: oklch(0.73 0.17 350);
--color-tfc17-green: #4ade80;
--color-tfc17-green: oklch(0.8 0.17 152);
--color-tfc17-ink: #faf5ff;
--color-tfc17-ink: oklch(0.98 0.015 300);
--color-tfc17-muted: #b3a8c2;
--color-tfc17-muted: oklch(0.76 0.035 300);
--color-tfc17-dim: #87809a;
--color-tfc17-dim: oklch(0.62 0.03 300);
--font-tfc17-sans: 'Sora', system-ui, sans-serif;
--font-tfc17-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tfc-17 {
width: 100%;
min-height: 100svh;
}
@media (prefers-reduced-motion: reduce) {
.tfc-17 * {
transition-duration: 1ms !important;
}
}
```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 Auto-Height Flip Card with CSS Grid Stacking & Cross-Fade
Source: https://codefronts.com/components/tailwind-css-flip-cards/tailwind-auto-height-flip-card-grid-stacking-cross-fade/
The CMS-safe variant: no pixel heights anywhere. Both faces live in the same grid cell via col-start-1 row-start-1, so the taller one sets the card's height and content of any length fits. The trade is real — this is a cross-fade with a rotation flourish, not a true 3D turn — and the port fixes the technique's accessibility hole by toggling aria-hidden on the face that is not showing.
## HTML
```html
<section class="tfc-17 w-full min-h-svh grid place-items-center content-center gap-6 bg-tfc17-bg font-tfc17-sans px-5 py-10 text-tfc17-ink">
<div class="text-center">
<h2 class="text-base font-bold text-white">Auto-height flip cards</h2>
<p class="mt-1 text-[11px] text-tfc17-muted">No fixed heights — each card grows to its tallest face</p>
<code class="mt-2 inline-block rounded border border-tfc17-violet/25 bg-tfc17-violet/15 px-2 py-0.5 font-tfc17-mono text-[11px] text-tfc17-violet">grid + col-start-1 row-start-1</code>
</div>
<div class="flex flex-wrap justify-center gap-5">
<div class="group relative grid w-[280px] rounded-2xl has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-tfc17-violet has-[:focus-visible]:outline-offset-4">
<input id="tfc-17-a" type="checkbox" class="peer sr-only">
<label for="tfc-17-a" class="absolute inset-0 z-20 cursor-pointer rounded-2xl"><span class="sr-only">Show the extended detail for card A</span></label>
<div class="col-start-1 row-start-1 rounded-2xl border border-tfc17-violet/20 bg-linear-145 from-tfc17-a1 to-tfc17-a2 p-6 transition-[opacity,transform] duration-500 ease-[cubic-bezier(.4,0,.2,1)] group-hover:opacity-0 group-hover:[transform:rotateY(90deg)] group-has-checked:opacity-0 group-has-checked:[transform:rotateY(90deg)] group-has-[:focus-visible]:opacity-0 group-has-[:focus-visible]:[transform:rotateY(90deg)] motion-reduce:transition-none">
<p class="mb-2.5 text-[9px] font-bold uppercase tracking-[0.15em] text-tfc17-violet">Dynamic card A</p>
<h3 class="mb-2 text-lg font-extrabold text-white">Short front</h3>
<p class="text-[13px] leading-[1.6] text-tfc17-muted">This front face has minimal content. The back has much more, and the card simply grows.</p>
<p class="mt-3.5 text-[10px] tracking-[0.08em] text-tfc17-dim">Hover, tap or press Space to expand →</p>
</div>
<div aria-hidden="true" class="col-start-1 row-start-1 rounded-2xl border border-tfc17-sky/20 bg-linear-145 from-tfc17-b1 to-tfc17-b2 p-6 opacity-0 [transform:rotateY(-90deg)] transition-[opacity,transform] duration-500 ease-[cubic-bezier(.4,0,.2,1)] group-hover:opacity-100 group-hover:[transform:rotateY(0deg)] group-has-checked:opacity-100 group-has-checked:[transform:rotateY(0deg)] group-has-[:focus-visible]:opacity-100 group-has-[:focus-visible]:[transform:rotateY(0deg)] motion-reduce:transition-none">
<p class="mb-2.5 text-[9px] font-bold uppercase tracking-[0.15em] text-tfc17-sky">Full detail · back</p>
<h3 class="mb-2.5 text-base font-extrabold text-white">Extended back content</h3>
<p class="text-[13px] leading-[1.7] text-tfc17-ink/70">The grid technique means this face can hold as much content as it needs and the card height adapts — no hardcoded pixels anywhere.</p>
<ul class="mt-2.5 flex flex-col gap-1.5 text-xs text-tfc17-ink/65">
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-sky">Both faces share one grid cell</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-sky">Opacity plus rotateY sells the turn</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-sky">The taller face sets the height</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-sky">Zero JavaScript</li>
</ul>
</div>
</div>
<div class="group relative grid w-[280px] rounded-2xl has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-tfc17-pink has-[:focus-visible]:outline-offset-4">
<input id="tfc-17-b" type="checkbox" class="peer sr-only">
<label for="tfc-17-b" class="absolute inset-0 z-20 cursor-pointer rounded-2xl"><span class="sr-only">Show the summary for card B</span></label>
<div class="col-start-1 row-start-1 rounded-2xl border border-tfc17-pink/20 bg-linear-145 from-tfc17-c1 to-tfc17-c2 p-6 transition-[opacity,transform] duration-500 ease-[cubic-bezier(.4,0,.2,1)] group-hover:opacity-0 group-hover:[transform:rotateY(90deg)] group-has-checked:opacity-0 group-has-checked:[transform:rotateY(90deg)] group-has-[:focus-visible]:opacity-0 group-has-[:focus-visible]:[transform:rotateY(90deg)] motion-reduce:transition-none">
<p class="mb-2.5 text-[9px] font-bold uppercase tracking-[0.15em] text-tfc17-pink">Dynamic card B</p>
<h3 class="mb-2 text-lg font-extrabold text-white">Longer front content</h3>
<p class="text-[13px] leading-[1.6] text-tfc17-muted">This card carries far more on the front — a paragraph, a list, and a closing note — and still no height is declared anywhere.</p>
<ul class="mt-3 flex flex-col gap-1.5 text-xs text-tfc17-ink/65">
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-pink">Point one with some detail</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-pink">Point two with some detail</li>
<li class="flex items-start gap-2 before:mt-1.5 before:size-1.5 before:shrink-0 before:rounded-full before:bg-tfc17-pink">Point three with some detail</li>
</ul>
<p class="mt-3.5 text-[10px] tracking-[0.08em] text-tfc17-dim">Hover, tap or press Space for the summary →</p>
</div>
<div aria-hidden="true" class="col-start-1 row-start-1 rounded-2xl border border-tfc17-green/20 bg-linear-145 from-tfc17-d1 to-tfc17-d2 p-6 opacity-0 [transform:rotateY(-90deg)] transition-[opacity,transform] duration-500 ease-[cubic-bezier(.4,0,.2,1)] group-hover:opacity-100 group-hover:[transform:rotateY(0deg)] group-has-checked:opacity-100 group-has-checked:[transform:rotateY(0deg)] group-has-[:focus-visible]:opacity-100 group-has-[:focus-visible]:[transform:rotateY(0deg)] motion-reduce:transition-none">
<p class="mb-2.5 text-[9px] font-bold uppercase tracking-[0.15em] text-tfc17-green">TL;DR · back</p>
<h3 class="mb-2.5 text-base font-extrabold text-white">Short summary</h3>
<p class="text-[13px] leading-[1.7] text-tfc17-ink/70">Each face owns its natural height. No JS, no fixed pixels, no clipped content.</p>
</div>
</div>
</div>
<p class="max-w-[580px] rounded-xl border border-tfc17-violet/15 bg-tfc17-violet/6 px-5 py-4 text-center text-xs leading-[1.7] text-tfc17-muted text-pretty">Both faces sit on <code class="rounded bg-tfc17-violet/12 px-1 font-tfc17-mono text-tfc17-violet">col-start-1 row-start-1</code>, so they overlap in one grid cell and the tallest one sets the card height. Opacity fades while <code class="rounded bg-tfc17-violet/12 px-1 font-tfc17-mono text-tfc17-violet">rotateY(±90deg)</code> adds depth — an auto-height flip with no JavaScript.</p>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tfc17-bg: #08060e;
--color-tfc17-bg: oklch(0.13 0.02 295);
--color-tfc17-a1: #130e28;
--color-tfc17-a1: oklch(0.19 0.05 295);
--color-tfc17-a2: #0a0718;
--color-tfc17-a2: oklch(0.14 0.035 295);
--color-tfc17-b1: #0e1a28;
--color-tfc17-b1: oklch(0.2 0.04 245);
--color-tfc17-b2: #080f18;
--color-tfc17-b2: oklch(0.15 0.025 245);
--color-tfc17-c1: #1a0e18;
--color-tfc17-c1: oklch(0.19 0.04 330);
--color-tfc17-c2: #0f0810;
--color-tfc17-c2: oklch(0.14 0.03 330);
--color-tfc17-d1: #0a1a0e;
--color-tfc17-d1: oklch(0.19 0.045 150);
--color-tfc17-d2: #060e08;
--color-tfc17-d2: oklch(0.13 0.025 150);
--color-tfc17-violet: #8b5cf6;
--color-tfc17-violet: oklch(0.63 0.2 295);
--color-tfc17-sky: #38bdf8;
--color-tfc17-sky: oklch(0.76 0.13 230);
--color-tfc17-pink: #f472b6;
--color-tfc17-pink: oklch(0.73 0.17 350);
--color-tfc17-green: #4ade80;
--color-tfc17-green: oklch(0.8 0.17 152);
--color-tfc17-ink: #faf5ff;
--color-tfc17-ink: oklch(0.98 0.015 300);
--color-tfc17-muted: #b3a8c2;
--color-tfc17-muted: oklch(0.76 0.035 300);
--color-tfc17-dim: #87809a;
--color-tfc17-dim: oklch(0.62 0.03 300);
--font-tfc17-sans: 'Sora', system-ui, sans-serif;
--font-tfc17-mono: 'JetBrains Mono', ui-monospace, monospace;
}
.tfc-17 {
width: 100%;
min-height: 100svh;
}
@media (prefers-reduced-motion: reduce) {
.tfc-17 * {
transition-duration: 1ms !important;
}
}
```How this works
Grid stacking is what removes the fixed height. The classic technique needs position:absolute on both faces, which takes them out of flow, which means the parent has no content to size against. Two grid items in one cell both contribute to the track size, so the wrapper grows to the taller face and the shorter one simply sits in the same box.
Why the flip is a cross-fade. Without absolute positioning there is no shared 3D box to rotate, so this turns opacity and a 90° rotation into the illusion. Tuning matters: opacity runs at 350ms and the rotation at 500ms on the same curve, so the outgoing face is gone before the incoming one squares up.
The accessibility fix the CSS version flagged and did not make. Both faces are visible to assistive tech at all times unless something says otherwise. Here the hidden face carries aria-hidden="true" and the state flips it, so a screen reader reads one card, not two.
Make it yours
- A third state: add another grid-cell child and cycle a
data-faceattribute. - Directional reveal: add
translate-x-5/-translate-x-5alongside the opacity change. - FAQ accordion: the same stacking with question on the front and a variable-length answer behind is the pattern's best use.
- Masonry wall: these cards have no fixed height, so they drop straight into a columns layout.
Gotchas — read before shipping
- No true 3D. If you need perspective depth, you need a fixed height and
preserve-3d— the two requirements are mutually exclusive. - Without
aria-hiddenon the hidden face, both faces are announced, and the card reads as duplicated nonsense. - Height changes between faces are instant unless you animate them; a
ResizeObserverplus an explicit height is the only way to tween that, and it needs JS. - Both faces must be grid ITEMS of the same container. Wrapping one in a div puts it in its own cell and the stack collapses into two rows.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Grid cell stacking is Baseline since 2017; :has() gates the tap and keyboard reveal.