23 CSS Flip Cards 21 / 23

Click-to-Flip JavaScript Toggle Card

A card that flips strictly on a click event using a JS-toggled helper class rather than a hover pseudo-class — ideal for touch and keyboard interfaces.

CSS + JS MIT licensed
Live Demo Open in tab
Open in playground

The code

<div class="fc-16">
  <div class="fc-16__meta">
    <h2>Click-to-Flip · JavaScript Toggle</h2>
    <p>Uses a CSS class toggle — no hover required</p>
    <div class="fc-16__click-pill"><span class="fc-16__click-dot"></span>Click anywhere on the card</div>
  </div>
  <div class="fc-16__scene" id="fc-16-scene">
    <div class="fc-16__card" id="fc-16-card">
      <div class="fc-16__front">
        <div class="fc-16__click-icon">
          <svg viewBox="0 0 24 24"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
        </div>
        <div class="fc-16__label">Click Trigger · JS Toggle</div>
        <div class="fc-16__title">Click to Flip</div>
        <div class="fc-16__sub">This card flips on click, not hover — great for mobile touch interfaces where hover states don't exist.</div>
        <button class="fc-16__click-cta" id="fc-16-flip-btn">
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="m9 18 6-6-6-6"/></svg>
          Click to Reveal
        </button>
      </div>
      <div class="fc-16__back">
        <div class="fc-16__back-icon">
          <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#4ade80" stroke-width="1.8"><polyline points="20 6 9 17 4 12"/></svg>
        </div>
        <div class="fc-16__back-title">Flip Complete ✓</div>
        <div class="fc-16__back-sub">The JS toggles <code style="color:#4ade80;background:rgba(0,0,0,.3);padding:1px 5px;border-radius:4px">.is-flipped</code> on the card element — no hover pseudo-class needed.</div>
        <div class="fc-16__code-block">
          <span class="fc-16__cmt">// toggle flip on click</span><br>
          card.<span class="fc-16__kw">classList</span>.<br>
          &nbsp;&nbsp;<span class="fc-16__str">toggle</span>(<span class="fc-16__str">'is-flipped'</span>);
        </div>
        <button class="fc-16__flip-back-btn" id="fc-16-back-btn">← Flip Back</button>
      </div>
    </div>
  </div>
</div>
.fc-16,.fc-16 *,.fc-16 *::before,.fc-16 *::after{box-sizing:border-box;margin:0;padding:0}
.fc-16 ::selection{background:#f97316;color:#fff}
.fc-16{
  --bg:#0c0800;--orange:#f97316;--yellow:#fbbf24;--white:#fffbf0;
  --card-w:340px;--card-h:420px;
  font-family:'Segoe UI',system-ui,sans-serif;
  background:radial-gradient(ellipse at 50% 40%,#1a0e00,#0c0800 65%);
  min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;
  padding:40px 20px;gap:20px;color:var(--white);
}
.fc-16__meta{text-align:center}
.fc-16__meta h2{font-size:16px;font-weight:700;color:var(--white)}
.fc-16__meta p{font-size:11px;color:rgba(255,251,240,.4);margin-top:4px}
.fc-16__click-pill{
  display:inline-flex;align-items:center;gap:6px;margin-top:8px;
  padding:6px 14px;border-radius:20px;
  background:rgba(249,115,22,.12);border:1px solid rgba(249,115,22,.25);
  font-size:11px;color:var(--orange);cursor:default;
}
.fc-16__click-dot{width:7px;height:7px;border-radius:50%;background:var(--orange);animation:fc-16-pulse 1.5s ease-in-out infinite}
@keyframes fc-16-pulse{0%,100%{opacity:.4;transform:scale(.8)}50%{opacity:1;transform:scale(1)}}
/* Card */
.fc-16__scene{width:var(--card-w);height:var(--card-h);perspective:1200px;cursor:pointer;flex-shrink:0}
.fc-16__card{
  width:100%;height:100%;position:relative;
  transform-style:preserve-3d;
  transition:transform .7s cubic-bezier(.4,0,.2,1);
}
/* JS toggle via class */
.fc-16__card.is-flipped{transform:rotateY(180deg)}
.fc-16__front,.fc-16__back{position:absolute;inset:0;border-radius:20px;backface-visibility:hidden;-webkit-backface-visibility:hidden;overflow:hidden}
/* FRONT */
.fc-16__front{
  background:linear-gradient(145deg,#1a0e00,#0f0900);
  border:1px solid rgba(249,115,22,.25);
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:16px;padding:36px 28px;
}
.fc-16__front::before{content:'';position:absolute;inset:0;background:radial-gradient(circle at 50% 40%,rgba(249,115,22,.08),transparent 60%)}
.fc-16__click-icon{
  width:80px;height:80px;border-radius:24px;
  background:linear-gradient(135deg,rgba(249,115,22,.15),rgba(251,191,36,.08));
  border:1px solid rgba(249,115,22,.25);
  display:flex;align-items:center;justify-content:center;
  position:relative;z-index:1;
  box-shadow:0 0 30px rgba(249,115,22,.12);
}
.fc-16__click-icon svg{width:36px;height:36px;stroke:var(--orange);stroke-width:1.8;fill:none}
.fc-16__label{font-size:10px;letter-spacing:.15em;text-transform:uppercase;color:var(--orange);font-weight:700;position:relative;z-index:1}
.fc-16__title{font-size:24px;font-weight:900;color:var(--white);text-align:center;position:relative;z-index:1}
.fc-16__sub{font-size:13px;color:rgba(255,251,240,.45);text-align:center;position:relative;z-index:1;line-height:1.6}
.fc-16__click-cta{
  position:relative;z-index:1;
  display:flex;align-items:center;gap:8px;margin-top:8px;
  padding:12px 24px;border-radius:12px;border:none;cursor:pointer;
  background:linear-gradient(135deg,var(--orange),var(--yellow));
  color:#000;font-size:13px;font-weight:800;letter-spacing:.04em;
  transition:transform .15s,box-shadow .15s;
  box-shadow:0 4px 20px rgba(249,115,22,.3);
}
.fc-16__click-cta:hover{box-shadow:0 8px 30px rgba(249,115,22,.5);filter:brightness(1.08)}
.fc-16__click-cta:active{filter:brightness(.95)}
/* BACK */
.fc-16__back{
  background:linear-gradient(145deg,#0e1a0a,#081008);
  border:1px solid rgba(74,222,128,.25);
  transform:rotateY(180deg);
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:16px;padding:36px 28px;
}
.fc-16__back::before{content:'';position:absolute;inset:0;background:radial-gradient(circle at 50% 40%,rgba(74,222,128,.06),transparent 60%)}
.fc-16__back-icon{
  width:80px;height:80px;border-radius:24px;
  background:rgba(74,222,128,.1);border:1px solid rgba(74,222,128,.2);
  display:flex;align-items:center;justify-content:center;position:relative;z-index:1;
}
.fc-16__back-title{font-size:22px;font-weight:900;color:var(--white);text-align:center;position:relative;z-index:1}
.fc-16__back-sub{font-size:13px;color:rgba(255,251,240,.5);text-align:center;position:relative;z-index:1;line-height:1.6}
.fc-16__code-block{
  position:relative;z-index:1;
  background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.08);
  border-radius:10px;padding:12px 16px;
  font-family:'Courier New',monospace;font-size:11px;color:rgba(255,251,240,.6);
  line-height:1.8;width:100%;text-align:left;
}
.fc-16__kw{color:#4ade80}.fc-16__str{color:#fb923c}.fc-16__cmt{color:rgba(255,251,240,.3)}
.fc-16__flip-back-btn{
  position:relative;z-index:1;
  padding:11px 24px;border-radius:12px;border:1px solid rgba(74,222,128,.3);
  background:rgba(74,222,128,.1);color:#4ade80;font-size:13px;font-weight:700;
  cursor:pointer;transition:background .2s;
}
.fc-16__flip-back-btn:hover{background:rgba(74,222,128,.18)}
@media(prefers-reduced-motion:reduce){.fc-16__card{transition:none}.fc-16__click-dot{animation:none}}
(function(){
  var card=document.getElementById('fc-16-card');
  var scene=document.getElementById('fc-16-scene');
  var backBtn=document.getElementById('fc-16-back-btn');
  scene.addEventListener('click',function(){card.classList.toggle('is-flipped')});
  backBtn.addEventListener('click',function(e){e.stopPropagation();card.classList.remove('is-flipped')});
})();

How this works

The flip is driven by toggling an .is-flipped class on the card element via a single card.classList.toggle() call in a click listener. The CSS applies transform: rotateY(180deg) whenever .is-flipped is present — the same 3D setup as the hover demos, just triggered differently. A separate Flip Back button calls classList.remove() with stopPropagation() to prevent the parent click from immediately re-toggling.

This pattern solves two real-world problems: touch interfaces where :hover is either unreliable or fires once on first tap, and accessibility contexts where keyboard users expect Enter or Space on a button to trigger actions. Making the trigger a native <button> element gives free keyboard access without extra ARIA.

Customize

  • Make the entire card area a click target by attaching the listener to the scene element rather than just the internal button, as shown in this demo.
  • Add keyboard support by listening for keydown with key === "Enter" || key === " " on the scene element alongside the click handler.
  • Persist the flip state across page loads by saving card.classList.contains("is-flipped") to localStorage and restoring it on DOMContentLoaded.
  • Animate the click CTA button with a brief scale(0.95) on :active for tactile press feedback before the card starts flipping.
  • Build a multi-card Memory game using this pattern — flip pairs on click, compare values, and lock matched pairs open with a permanent class.

Watch out for

  • Adding the click listener to the scene wrapper means any click on links inside will also trigger the flip — always stopPropagation() on inner interactive elements.
  • Do not add :hover flip rules alongside the JS class toggle — the two triggers fight each other on desktop when the user mouses over then clicks.
  • The .is-flipped class approach breaks if the card is inside a framework that resets the DOM on re-render — store flip state in component state, not on the DOM node.

Browser support

ChromeSafariFirefoxEdge
36+ 9+ 16+ 36+

classList.toggle is universally supported; no modern-browser-only APIs are used in this demo.

Search CodeFronts

Loading…