23 Tailwind CSS Flip Cards
16 / 23

All 23 in 23 Tailwind CSS Flip Cards

  1. 01Tailwind Product Comparison Flip Card with Hover, Tap & Keyboard Reveal
  2. 02Tailwind Pricing Plan Flip Card with Gradient Price & Feature Reveal
  3. 03Tailwind Gift Card Flip with Copy-to-Clipboard Coupon Code Reveal
  4. 04Tailwind Meet the Team Flip Card with Bio, Socials & Contact Reveal
  5. 05Tailwind Portfolio Flip Card with Animated Skill Bar Proficiency Meters
  6. 06Tailwind Language Flashcard Flip with Phonetics, Example & Memory Tip
  7. 07Tailwind Trivia Quiz Flip Card with Answer Reveal & Score Readout
  8. 08Tailwind Gaming Trading Card Flip with Character Stats & Rarity Glow
  9. 09Tailwind Photo Gallery Flip Card with EXIF Metadata & Download CTA
  10. 10Tailwind Video Thumbnail Flip Card with Playlist, Stats & Watch CTA
  11. 11Tailwind Testimonial Flip Card with Star Rating & Reviewer Profile
  12. 12Tailwind Digital Business Card Flip with vCard Contacts & QR Panel
  13. 13Tailwind Restaurant Menu Card with Sliding Ingredient & Nutrition Reveal
  14. 14Tailwind Process Steps Flip Card Grid with Task Lists & Timelines
  15. 15Tailwind Vertical Axis Flip Card with rotateX Top-to-Bottom Reveal
  16. 16Tailwind Click-to-Flip Card with Persisted State & Keyboard Control
  17. 17Tailwind Auto-Height Flip Card with CSS Grid Stacking & Cross-Fade
  18. 18Tailwind Responsive Flip Card Grid with Per-Card 3D Perspective
  19. 19Tailwind Luxury Product Card with Hinged Book-Open 3D Unfold
  20. 20Tailwind Travel Postcard Flip Card with Diagonal rotate3d Twist
  21. 21Tailwind Album Card with clip-path Iris Aperture Reveal (No 3D)
  22. 22Tailwind Event Ticket Card with Bi-Fold Stub Reveal & QR Panel
  23. 23Tailwind Scratch Card with Diagonal clip-path Peel & Prize Reveal
Tailwind + JSMIT licensed

Tailwind Click-to-Flip Card with Persisted State & Keyboard Control

The explicitly scripted variant: no hover trigger at all, a real <button> that owns aria-expanded, and the flipped state persisted to localStorage so a reload does not throw the reader back to the front. State lives in a data-flipped attribute, which Tailwind reads with data-flipped: — no .is-flipped class, no string matching in 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="tfc-16 w-full min-h-svh grid place-items-center content-center gap-5 bg-tfc16-bg font-tfc16-sans px-5 py-10 text-tfc16-ink">
  <div class="text-center">
    <h2 class="text-base font-bold text-white">Click-to-flip &middot; scripted toggle</h2>
    <p class="mt-1 text-[11px] text-tfc16-muted">No hover trigger &mdash; the state is a data attribute, and it survives a reload</p>
    <p class="mt-2 inline-flex items-center gap-1.5 rounded-full border border-tfc16-orange/25 bg-tfc16-orange/12 px-3.5 py-1.5 text-[11px] text-tfc16-orange">
      <span class="size-1.5 rounded-full bg-tfc16-orange animate-tfc16-pulse motion-reduce:animate-none"></span>Use the button, or press Enter
    </p>
  </div>

  <div class="relative mx-auto h-[430px] w-[340px] max-w-full [perspective:1200px]">
    <div id="tfc-16-card" data-flipped="false" class="relative h-full w-full transition-transform duration-700 ease-[cubic-bezier(.4,0,.2,1)] [transform-style:preserve-3d] data-[flipped=true]:[transform:rotateY(180deg)] motion-reduce:transition-none">

      <div class="absolute inset-0 flex flex-col items-center justify-center gap-4 rounded-2xl overflow-hidden px-7 py-9 bg-linear-145 from-tfc16-panel to-tfc16-panel2 border border-tfc16-orange/25 shadow-[0_1px_2px_oklch(0_0_0/0.5),0_22px_48px_-18px_oklch(0.55_0.18_45/0.4)] [backface-visibility:hidden] [-webkit-backface-visibility:hidden] before:absolute before:inset-0 before:bg-[radial-gradient(circle_at_50%_40%,oklch(0.7_0.19_45/0.08),transparent_60%)]">
        <span class="relative z-1 grid size-20 place-items-center rounded-3xl border border-tfc16-orange/25 bg-linear-135 from-tfc16-orange/15 to-tfc16-yellow/8 shadow-[0_0_30px_oklch(0.7_0.19_45/0.12)]">
          <svg viewBox="0 0 24 24" class="size-9 fill-none stroke-tfc16-orange stroke-[1.8]" aria-hidden="true"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
        </span>
        <p class="relative z-1 text-[10px] font-bold uppercase tracking-[0.15em] text-tfc16-orange">Click trigger &middot; scripted</p>
        <h3 class="relative z-1 text-center text-2xl font-black tracking-[-0.02em] text-white">Click to flip</h3>
        <p class="relative z-1 text-center text-[13px] leading-[1.6] text-tfc16-muted text-pretty">This card flips on click, never on hover &mdash; which is the only version that behaves the same on a phone as on a desktop.</p>
        <button id="tfc-16-toggle" type="button" aria-expanded="false" aria-controls="tfc-16-back" class="relative z-30 mt-2 inline-flex min-h-11 items-center gap-2 rounded-xl bg-linear-135 from-tfc16-orange to-tfc16-yellow px-6 text-[13px] font-extrabold tracking-[0.04em] text-black shadow-[0_4px_20px_oklch(0.7_0.19_45/0.3)] transition-[filter,box-shadow] duration-150 hover:brightness-110 active:brightness-95 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>
          Click to reveal
        </button>
      </div>

      <div id="tfc-16-back" class="absolute inset-0 flex flex-col items-center justify-center gap-4 rounded-2xl overflow-hidden px-7 py-9 bg-linear-145 from-tfc16-back to-tfc16-back2 border border-tfc16-green/25 shadow-[0_1px_2px_oklch(0_0_0/0.5),0_22px_48px_-18px_oklch(0.6_0.17_150/0.35)] [transform:rotateY(180deg)] [backface-visibility:hidden] [-webkit-backface-visibility:hidden]">
        <span class="grid size-20 place-items-center rounded-3xl border border-tfc16-green/20 bg-tfc16-green/10">
          <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="text-tfc16-green" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>
        </span>
        <h3 class="text-center text-[22px] font-black tracking-[-0.02em] text-white">Flip complete</h3>
        <p class="text-center text-[13px] leading-[1.6] text-tfc16-muted text-pretty">The script sets <code class="rounded bg-black/30 px-1.5 py-0.5 font-tfc16-mono text-tfc16-green">data-flipped</code> and Tailwind's <code class="rounded bg-black/30 px-1.5 py-0.5 font-tfc16-mono text-tfc16-green">data-[flipped=true]:</code> variant styles it.</p>
        <pre class="w-full rounded-xl border border-white/8 bg-black/40 px-4 py-3 text-left font-tfc16-mono text-[11px] leading-[1.7] text-tfc16-ink/60"><span class="text-tfc16-ink/30">// one attribute, one source of truth</span>
card.dataset.<span class="text-tfc16-green">flipped</span> = <span class="text-tfc16-orangeL">'true'</span>;</pre>
        <button id="tfc-16-back-btn" type="button" class="relative z-30 min-h-11 rounded-xl border border-tfc16-green/30 bg-tfc16-green/10 px-6 text-[13px] font-bold text-tfc16-green transition-colors duration-200 hover:bg-tfc16-green/20 focus-visible:outline-2 focus-visible:outline-tfc16-green focus-visible:outline-offset-2">&larr; Flip back</button>
      </div>
    </div>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tfc16-bg: #0c0800;
  --color-tfc16-bg: oklch(0.14 0.02 70);
  --color-tfc16-panel: #1a0e00;
  --color-tfc16-panel: oklch(0.19 0.045 60);
  --color-tfc16-panel2: #0f0900;
  --color-tfc16-panel2: oklch(0.15 0.03 65);
  --color-tfc16-back: #0e1a0a;
  --color-tfc16-back: oklch(0.2 0.045 145);
  --color-tfc16-back2: #081008;
  --color-tfc16-back2: oklch(0.14 0.025 150);
  --color-tfc16-orange: #f97316;
  --color-tfc16-orange: oklch(0.7 0.19 45);
  --color-tfc16-orangeL: #fb923c;
  --color-tfc16-orangeL: oklch(0.76 0.16 55);
  --color-tfc16-yellow: #fbbf24;
  --color-tfc16-yellow: oklch(0.83 0.16 82);
  --color-tfc16-green: #4ade80;
  --color-tfc16-green: oklch(0.8 0.17 152);
  --color-tfc16-ink: #fffbf0;
  --color-tfc16-ink: oklch(0.99 0.015 90);
  --color-tfc16-muted: #bdb09c;
  --color-tfc16-muted: oklch(0.77 0.03 85);
  --font-tfc16-sans: 'Sora', system-ui, sans-serif;
  --font-tfc16-mono: 'JetBrains Mono', ui-monospace, monospace;
  --animate-tfc16-pulse: tfc16-pulse 1.5s ease-in-out infinite;
}

@keyframes tfc16-pulse {
  0%,
    100% {
    opacity: 0.4;
    transform: scale(0.8);
  }

  50% {
    opacity: 1;
    transform: scale(1);
  }
}

.tfc-16 {
  width: 100%;
  min-height: 100svh;
}

@media (prefers-reduced-motion: reduce) {
  .tfc-16 * {
    animation: none !important;
    transition-duration: 1ms !important;
  }
}
(() => {
  const card = document.getElementById('tfc-16-card');
  const toggle = document.getElementById('tfc-16-toggle');
  const backBtn = document.getElementById('tfc-16-back-btn');
  const KEY = 'tfc-16-flipped';
  if (!card) return;

  const set = (flipped) => {
    card.dataset.flipped = String(flipped);
    toggle.setAttribute('aria-expanded', String(flipped));
    try { localStorage.setItem(KEY, String(flipped)); } catch {}
  };

  // Restore without animating the restore frame.
  try {
    if (localStorage.getItem(KEY) === 'true') {
      card.classList.add('transition-none');
      set(true);
      requestAnimationFrame(() => card.classList.remove('transition-none'));
    }
  } catch {}

  toggle.addEventListener('click', () => set(card.dataset.flipped !== 'true'));
  backBtn.addEventListener('click', (e) => { e.stopPropagation(); set(false); });
})();

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-tfc16-bg: #0c0800;
  --color-tfc16-bg: oklch(0.14 0.02 70);
  --color-tfc16-panel: #1a0e00;
  --color-tfc16-panel: oklch(0.19 0.045 60);
  --color-tfc16-panel2: #0f0900;
  --color-tfc16-panel2: oklch(0.15 0.03 65);
  --color-tfc16-back: #0e1a0a;
  --color-tfc16-back: oklch(0.2 0.045 145);
  --color-tfc16-back2: #081008;
  --color-tfc16-back2: oklch(0.14 0.025 150);
  --color-tfc16-orange: #f97316;
  --color-tfc16-orange: oklch(0.7 0.19 45);
  --color-tfc16-orangeL: #fb923c;
  --color-tfc16-orangeL: oklch(0.76 0.16 55);
  --color-tfc16-yellow: #fbbf24;
  --color-tfc16-yellow: oklch(0.83 0.16 82);
  --color-tfc16-green: #4ade80;
  --color-tfc16-green: oklch(0.8 0.17 152);
  --color-tfc16-ink: #fffbf0;
  --color-tfc16-ink: oklch(0.99 0.015 90);
  --color-tfc16-muted: #bdb09c;
  --color-tfc16-muted: oklch(0.77 0.03 85);
  --font-tfc16-sans: 'Sora', system-ui, sans-serif;
  --font-tfc16-mono: 'JetBrains Mono', ui-monospace, monospace;
  --animate-tfc16-pulse: tfc16-pulse 1.5s ease-in-out infinite;
}

@keyframes tfc16-pulse {
  0%,
    100% {
    opacity: 0.4;
    transform: scale(0.8);
  }

  50% {
    opacity: 1;
    transform: scale(1);
  }
}

.tfc-16 {
  width: 100%;
  min-height: 100svh;
}

@media (prefers-reduced-motion: reduce) {
  .tfc-16 * {
    animation: none !important;
    transition-duration: 1ms !important;
  }
}
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=Sora:wght@400;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap">

What changed in the port

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

  • classList.toggle('is-flipped') became dataset.flipped plus the data-[flipped=true]: variant. Same line count, but the state is now a value rather than the presence of a string in a list.
  • This is the only demo with no hover trigger, on purpose: it is the one that answers "what do I ship when the card must behave identically on every input device".
  • UI change from the CSS original: the trigger is a labelled button with aria-expanded and aria-controls, the state persists across reloads without a load-time spin, and the code sample is a real <pre> instead of <br>-separated spans.
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 Click-to-Flip Card with Persisted State & Keyboard Control
Source: https://codefronts.com/components/tailwind-css-flip-cards/tailwind-click-to-flip-card-persisted-state-keyboard/

The explicitly scripted variant: no hover trigger at all, a real <button> that owns aria-expanded, and the flipped state persisted to localStorage so a reload does not throw the reader back to the front. State lives in a data-flipped attribute, which Tailwind reads with data-flipped: &mdash; no .is-flipped class, no string matching in the stylesheet.
## HTML
```html
<section class="tfc-16 w-full min-h-svh grid place-items-center content-center gap-5 bg-tfc16-bg font-tfc16-sans px-5 py-10 text-tfc16-ink">
  <div class="text-center">
    <h2 class="text-base font-bold text-white">Click-to-flip &middot; scripted toggle</h2>
    <p class="mt-1 text-[11px] text-tfc16-muted">No hover trigger &mdash; the state is a data attribute, and it survives a reload</p>
    <p class="mt-2 inline-flex items-center gap-1.5 rounded-full border border-tfc16-orange/25 bg-tfc16-orange/12 px-3.5 py-1.5 text-[11px] text-tfc16-orange">
      <span class="size-1.5 rounded-full bg-tfc16-orange animate-tfc16-pulse motion-reduce:animate-none"></span>Use the button, or press Enter
    </p>
  </div>

  <div class="relative mx-auto h-[430px] w-[340px] max-w-full [perspective:1200px]">
    <div id="tfc-16-card" data-flipped="false" class="relative h-full w-full transition-transform duration-700 ease-[cubic-bezier(.4,0,.2,1)] [transform-style:preserve-3d] data-[flipped=true]:[transform:rotateY(180deg)] motion-reduce:transition-none">

      <div class="absolute inset-0 flex flex-col items-center justify-center gap-4 rounded-2xl overflow-hidden px-7 py-9 bg-linear-145 from-tfc16-panel to-tfc16-panel2 border border-tfc16-orange/25 shadow-[0_1px_2px_oklch(0_0_0/0.5),0_22px_48px_-18px_oklch(0.55_0.18_45/0.4)] [backface-visibility:hidden] [-webkit-backface-visibility:hidden] before:absolute before:inset-0 before:bg-[radial-gradient(circle_at_50%_40%,oklch(0.7_0.19_45/0.08),transparent_60%)]">
        <span class="relative z-1 grid size-20 place-items-center rounded-3xl border border-tfc16-orange/25 bg-linear-135 from-tfc16-orange/15 to-tfc16-yellow/8 shadow-[0_0_30px_oklch(0.7_0.19_45/0.12)]">
          <svg viewBox="0 0 24 24" class="size-9 fill-none stroke-tfc16-orange stroke-[1.8]" aria-hidden="true"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
        </span>
        <p class="relative z-1 text-[10px] font-bold uppercase tracking-[0.15em] text-tfc16-orange">Click trigger &middot; scripted</p>
        <h3 class="relative z-1 text-center text-2xl font-black tracking-[-0.02em] text-white">Click to flip</h3>
        <p class="relative z-1 text-center text-[13px] leading-[1.6] text-tfc16-muted text-pretty">This card flips on click, never on hover &mdash; which is the only version that behaves the same on a phone as on a desktop.</p>
        <button id="tfc-16-toggle" type="button" aria-expanded="false" aria-controls="tfc-16-back" class="relative z-30 mt-2 inline-flex min-h-11 items-center gap-2 rounded-xl bg-linear-135 from-tfc16-orange to-tfc16-yellow px-6 text-[13px] font-extrabold tracking-[0.04em] text-black shadow-[0_4px_20px_oklch(0.7_0.19_45/0.3)] transition-[filter,box-shadow] duration-150 hover:brightness-110 active:brightness-95 focus-visible:outline-2 focus-visible:outline-white focus-visible:outline-offset-2">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>
          Click to reveal
        </button>
      </div>

      <div id="tfc-16-back" class="absolute inset-0 flex flex-col items-center justify-center gap-4 rounded-2xl overflow-hidden px-7 py-9 bg-linear-145 from-tfc16-back to-tfc16-back2 border border-tfc16-green/25 shadow-[0_1px_2px_oklch(0_0_0/0.5),0_22px_48px_-18px_oklch(0.6_0.17_150/0.35)] [transform:rotateY(180deg)] [backface-visibility:hidden] [-webkit-backface-visibility:hidden]">
        <span class="grid size-20 place-items-center rounded-3xl border border-tfc16-green/20 bg-tfc16-green/10">
          <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="text-tfc16-green" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>
        </span>
        <h3 class="text-center text-[22px] font-black tracking-[-0.02em] text-white">Flip complete</h3>
        <p class="text-center text-[13px] leading-[1.6] text-tfc16-muted text-pretty">The script sets <code class="rounded bg-black/30 px-1.5 py-0.5 font-tfc16-mono text-tfc16-green">data-flipped</code> and Tailwind's <code class="rounded bg-black/30 px-1.5 py-0.5 font-tfc16-mono text-tfc16-green">data-[flipped=true]:</code> variant styles it.</p>
        <pre class="w-full rounded-xl border border-white/8 bg-black/40 px-4 py-3 text-left font-tfc16-mono text-[11px] leading-[1.7] text-tfc16-ink/60"><span class="text-tfc16-ink/30">// one attribute, one source of truth</span>
card.dataset.<span class="text-tfc16-green">flipped</span> = <span class="text-tfc16-orangeL">'true'</span>;</pre>
        <button id="tfc-16-back-btn" type="button" class="relative z-30 min-h-11 rounded-xl border border-tfc16-green/30 bg-tfc16-green/10 px-6 text-[13px] font-bold text-tfc16-green transition-colors duration-200 hover:bg-tfc16-green/20 focus-visible:outline-2 focus-visible:outline-tfc16-green focus-visible:outline-offset-2">&larr; Flip back</button>
      </div>
    </div>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tfc16-bg: #0c0800;
  --color-tfc16-bg: oklch(0.14 0.02 70);
  --color-tfc16-panel: #1a0e00;
  --color-tfc16-panel: oklch(0.19 0.045 60);
  --color-tfc16-panel2: #0f0900;
  --color-tfc16-panel2: oklch(0.15 0.03 65);
  --color-tfc16-back: #0e1a0a;
  --color-tfc16-back: oklch(0.2 0.045 145);
  --color-tfc16-back2: #081008;
  --color-tfc16-back2: oklch(0.14 0.025 150);
  --color-tfc16-orange: #f97316;
  --color-tfc16-orange: oklch(0.7 0.19 45);
  --color-tfc16-orangeL: #fb923c;
  --color-tfc16-orangeL: oklch(0.76 0.16 55);
  --color-tfc16-yellow: #fbbf24;
  --color-tfc16-yellow: oklch(0.83 0.16 82);
  --color-tfc16-green: #4ade80;
  --color-tfc16-green: oklch(0.8 0.17 152);
  --color-tfc16-ink: #fffbf0;
  --color-tfc16-ink: oklch(0.99 0.015 90);
  --color-tfc16-muted: #bdb09c;
  --color-tfc16-muted: oklch(0.77 0.03 85);
  --font-tfc16-sans: 'Sora', system-ui, sans-serif;
  --font-tfc16-mono: 'JetBrains Mono', ui-monospace, monospace;
  --animate-tfc16-pulse: tfc16-pulse 1.5s ease-in-out infinite;
}

@keyframes tfc16-pulse {
  0%,
    100% {
    opacity: 0.4;
    transform: scale(0.8);
  }

  50% {
    opacity: 1;
    transform: scale(1);
  }
}

.tfc-16 {
  width: 100%;
  min-height: 100svh;
}

@media (prefers-reduced-motion: reduce) {
  .tfc-16 * {
    animation: none !important;
    transition-duration: 1ms !important;
  }
}
```

## JavaScript
```js
(() => {
  const card = document.getElementById('tfc-16-card');
  const toggle = document.getElementById('tfc-16-toggle');
  const backBtn = document.getElementById('tfc-16-back-btn');
  const KEY = 'tfc-16-flipped';
  if (!card) return;

  const set = (flipped) => {
    card.dataset.flipped = String(flipped);
    toggle.setAttribute('aria-expanded', String(flipped));
    try { localStorage.setItem(KEY, String(flipped)); } catch {}
  };

  // Restore without animating the restore frame.
  try {
    if (localStorage.getItem(KEY) === 'true') {
      card.classList.add('transition-none');
      set(true);
      requestAnimationFrame(() => card.classList.remove('transition-none'));
    }
  } catch {}

  toggle.addEventListener('click', () => set(card.dataset.flipped !== 'true'));
  backBtn.addEventListener('click', (e) => { e.stopPropagation(); set(false); });
})();
```

How this works

The attribute IS the state. card.dataset.flipped = String(next) and the data-flipped: variant does the rest. Compared with classList.toggle('is-flipped') the win is that the state is inspectable, serialisable and unambiguous — a class list can accumulate; an attribute has one value.

ARIA is not optional on a scripted disclosure. The button carries aria-expanded and aria-controls pointing at the back face, so assistive tech knows a region was revealed rather than that something moved.

Restoring state without a flash. On load the script sets the attribute while a transition-none class is present, then removes that class on the next frame — otherwise the card visibly spins itself on every page load.

Make it yours

  • Drop persistence by deleting the two localStorage lines; the attribute toggle stands alone.
  • Memory game: give every card the same handler and compare data-value attributes on the pair that is open.
  • Make the whole card clickable again by adding the same handler to the scene — but keep stopPropagation on the inner buttons.
  • Hover as a bonus trigger is deliberately absent here; adding it back reintroduces the desktop fight this demo exists to avoid.

Gotchas — read before shipping

  • Never combine this with a hover: flip. On desktop the two triggers fight: hover flips, click flips back, and the card ends up in the state the user did not ask for.
  • Restoring persisted state without suppressing the transition makes every page load animate. Set the attribute, then re-enable transitions on the next frame.
  • Inner buttons need stopPropagation if the scene also listens, or Flip Back immediately re-flips.
  • In a framework that re-renders the DOM, keep the flag in component state and render it into data-flipped — mutating the node directly gets clobbered.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

No :has() needed — this demo's trigger is scripted, so it works wherever Tailwind v4's sheet does.

Techniques used in this demo

Plain CSSno Tailwind surface

HTML & JSno Tailwind involved

Search CodeFronts

Loading…