Back to CSS Stacked Cards Swipeable / Click-to-Front Stacked Cards CSS + JS
Share
HTML
<div class="scd-flash">
  <div class="scd-flash__stage" data-scd-flash="stage">
    <div class="scd-flash__counter"><span data-scd-flash="pos">01 / 04</span><span class="scd-flash__prog"><i data-scd-flash="bar" style="width:0%"></i></span><span>DECK</span></div>
    <div class="scd-flash__deck" data-scd-flash="deck">
      <div class="scd-flash__card scd-flash__card--g4"><div class="scd-flash__grad"></div><div class="scd-flash__glow"></div><div class="scd-flash__sheen"></div>
        <span class="scd-flash__verdict scd-flash__verdict--know">KNOW &check;</span><span class="scd-flash__verdict scd-flash__verdict--again">AGAIN &#8635;</span>
        <div class="scd-flash__face"><span class="scd-flash__tag">Biology</span><div class="scd-flash__term">Mitosis</div><div><p class="scd-flash__def">Cell division producing two genetically identical daughter cells.</p><br><span class="scd-flash__fliphint">drag &middot; or click for next</span></div></div></div>
      <div class="scd-flash__card scd-flash__card--g3"><div class="scd-flash__grad"></div><div class="scd-flash__glow"></div><div class="scd-flash__sheen"></div>
        <span class="scd-flash__verdict scd-flash__verdict--know">KNOW &check;</span><span class="scd-flash__verdict scd-flash__verdict--again">AGAIN &#8635;</span>
        <div class="scd-flash__face"><span class="scd-flash__tag">Physics</span><div class="scd-flash__term">Photon</div><div><p class="scd-flash__def">A discrete quantum, or packet, of electromagnetic energy &mdash; light.</p><br><span class="scd-flash__fliphint">drag &middot; or click for next</span></div></div></div>
      <div class="scd-flash__card scd-flash__card--g2"><div class="scd-flash__grad"></div><div class="scd-flash__glow"></div><div class="scd-flash__sheen"></div>
        <span class="scd-flash__verdict scd-flash__verdict--know">KNOW &check;</span><span class="scd-flash__verdict scd-flash__verdict--again">AGAIN &#8635;</span>
        <div class="scd-flash__face"><span class="scd-flash__tag">Thermo</span><div class="scd-flash__term">Entropy</div><div><p class="scd-flash__def">A measure of disorder; the tendency of systems toward equilibrium.</p><br><span class="scd-flash__fliphint">drag &middot; or click for next</span></div></div></div>
      <div class="scd-flash__card scd-flash__card--g1"><div class="scd-flash__grad"></div><div class="scd-flash__glow"></div><div class="scd-flash__sheen"></div>
        <span class="scd-flash__verdict scd-flash__verdict--know">KNOW &check;</span><span class="scd-flash__verdict scd-flash__verdict--again">AGAIN &#8635;</span>
        <div class="scd-flash__face"><span class="scd-flash__tag">Biology</span><div class="scd-flash__term">Osmosis</div><div><p class="scd-flash__def">Water moving across a semipermeable membrane toward higher solute.</p><br><span class="scd-flash__fliphint">drag &middot; or click for next</span></div></div></div>
    </div>
    <div class="scd-flash__hint">swipe <b>right = know</b> &middot; <i>left = review</i> &middot; click to skip</div>
  </div>
</div>
CSS
@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@300;400;500;600;700&family=Geist+Mono:wght@400&display=swap');

.scd-flash, .scd-flash *, .scd-flash *::before, .scd-flash *::after { box-sizing: border-box; margin: 0; padding: 0; }

.scd-flash {
  position: relative;
  min-height: 460px;
  display: grid;
  place-items: center;
  background: #05060a;
  font-family: 'Geist', sans-serif;
}
.scd-flash::before {
  content: '';
  position: absolute; inset: -30%;
  background:
    radial-gradient(40% 40% at 22% 28%, rgba(56,189,248,.30), transparent 70%),
    radial-gradient(45% 45% at 78% 25%, rgba(168,85,247,.28), transparent 70%),
    radial-gradient(50% 50% at 50% 82%, rgba(244,114,182,.24), transparent 70%);
  filter: blur(26px);
  animation: scd-flash-drift 19s ease-in-out infinite alternate;
  pointer-events: none;
}
@keyframes scd-flash-drift {
  0% { transform: translate(-3%,-2%) scale(1); }
  100% { transform: translate(4%,3%) scale(1.1); }
}
.scd-flash::after {
  content: '';
  position: absolute; inset: 0;
  background: repeating-linear-gradient(0deg, rgba(255,255,255,.02) 0 1px, transparent 1px 3px);
  mix-blend-mode: overlay;
  pointer-events: none;
}

.scd-flash__stage { position: relative; width: 280px; height: 380px; perspective: 1300px; z-index: 1; }
.scd-flash__counter {
  position: absolute; top: -46px; left: 0; right: 0;
  display: flex; align-items: center; justify-content: space-between;
  font-family: 'Geist Mono', monospace;
  font-size: .72rem; letter-spacing: .15em;
  color: rgba(255,255,255,.55);
}
.scd-flash__prog { flex: 1; height: 3px; margin: 0 12px; background: rgba(255,255,255,.15); border-radius: 3px; overflow: hidden; }
.scd-flash__prog i { display: block; height: 100%; background: #fff; border-radius: 3px; transition: width .4s; box-shadow: 0 0 10px rgba(255,255,255,.6); }

.scd-flash__deck { position: relative; width: 100%; height: 100%; transform-style: preserve-3d; }
.scd-flash__card {
  position: absolute; inset: 0;
  border-radius: 26px;
  transform-style: preserve-3d;
  cursor: grab;
  user-select: none;
  overflow: hidden;
  box-shadow: 0 30px 60px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,.09) inset;
  transition: transform .5s cubic-bezier(.16,1,.3,1), opacity .5s, box-shadow .5s;
}
.scd-flash__card.scd-flash__card--drag { transition: none; cursor: grabbing; }
.scd-flash__grad { position: absolute; inset: 0; background-size: 200% 200%; animation: scd-flash-flow 8s ease infinite; }
@keyframes scd-flash-flow {
  0% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}
.scd-flash__glow {
  position: absolute; inset: 0;
  background: radial-gradient(260px circle at var(--mx,50%) var(--my,40%), rgba(255,255,255,.4), transparent 60%);
  mix-blend-mode: soft-light;
  opacity: 0;
  transition: opacity .4s;
}
.scd-flash__card:hover .scd-flash__glow { opacity: 1; }
.scd-flash__sheen {
  position: absolute; top: -50%; left: -30%;
  width: 55%; height: 200%;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,.45), transparent);
  transform: rotate(18deg) translateX(-260px);
  transition: transform .9s cubic-bezier(.16,1,.3,1);
}

.scd-flash__verdict {
  position: absolute; top: 24px;
  padding: 8px 16px;
  border-radius: 12px;
  font-family: 'Geist Mono', monospace;
  font-size: .85rem; font-weight: 700;
  letter-spacing: .1em;
  border: 2px solid;
  opacity: 0;
  transition: opacity .15s;
  backdrop-filter: blur(4px);
}
.scd-flash__verdict--know { right: 22px; color: #86efac; border-color: #86efac; transform: rotate(10deg); }
.scd-flash__verdict--again { left: 22px; color: #fca5a5; border-color: #fca5a5; transform: rotate(-10deg); }

.scd-flash__face {
  position: absolute; inset: 0;
  padding: 30px;
  display: flex; flex-direction: column; justify-content: space-between;
  transform: translateZ(40px);
  color: #fff;
}
.scd-flash__tag {
  font-family: 'Geist Mono', monospace;
  font-size: .62rem; letter-spacing: .22em; text-transform: uppercase;
  padding: 6px 12px; border-radius: 999px;
  background: rgba(255,255,255,.16);
  border: 1px solid rgba(255,255,255,.24);
  align-self: flex-start;
  backdrop-filter: blur(6px);
}
.scd-flash__term {
  font-family: 'Instrument Serif', serif;
  font-size: 2.7rem; line-height: 1; letter-spacing: -.01em;
  text-shadow: 0 3px 18px rgba(0,0,0,.3);
}
.scd-flash__def { font-size: .9rem; opacity: .9; line-height: 1.5; font-weight: 300; }
.scd-flash__fliphint { font-family: 'Geist Mono', monospace; font-size: .62rem; letter-spacing: .18em; opacity: .6; }

.scd-flash__card--g1 .scd-flash__grad { background: linear-gradient(135deg,#831843,#db2777 45%,#fb7185); }
.scd-flash__card--g2 .scd-flash__grad { background: linear-gradient(135deg,#0c4a6e,#0284c7 45%,#38bdf8); }
.scd-flash__card--g3 .scd-flash__grad { background: linear-gradient(135deg,#064e3b,#059669 50%,#34d399); }
.scd-flash__card--g4 .scd-flash__grad { background: linear-gradient(135deg,#581c87,#7c3aed 45%,#a78bfa); }

.scd-flash__hint {
  position: absolute; bottom: -46px; left: 0; right: 0;
  text-align: center;
  font-family: 'Geist Mono', monospace;
  color: rgba(255,255,255,.5);
  font-size: .7rem;
  letter-spacing: .12em;
}
.scd-flash__hint b { color: #86efac; }
.scd-flash__hint i { color: #fca5a5; font-style: normal; }

@media (prefers-reduced-motion: reduce) {
  .scd-flash::before,
  .scd-flash__grad,
  .scd-flash__sheen,
  .scd-flash__card,
  .scd-flash__prog i { animation: none !important; transition: none !important; }
}
JS
(() => {
  const root = document.querySelector('.scd-flash');
  if (!root) return;
  const stage = root.querySelector('[data-scd-flash="stage"]');
  const deck = root.querySelector('[data-scd-flash="deck"]');
  const posEl = root.querySelector('[data-scd-flash="pos"]');
  const barEl = root.querySelector('[data-scd-flash="bar"]');
  if (!stage || !deck) return;

  const total = deck.querySelectorAll('.scd-flash__card').length;
  let done = 0;

  function meter() {
    const current = Math.min(done + 1, total);
    if (posEl) posEl.textContent = String(current).padStart(2, '0') + ' / ' + String(total).padStart(2, '0');
    if (barEl) barEl.style.width = (done / total * 100) + '%';
  }

  function layout(instant) {
    const cards = [...deck.querySelectorAll('.scd-flash__card')];
    cards.forEach((c, i) => {
      const d = cards.length - 1 - i;
      const tf = `translateY(${d * -14}px) translateZ(${-d * 30}px) scale(${1 - d * .05})`;
      if (instant) {
        const t = c.style.transition;
        c.style.transition = 'none';
        c.style.transform = tf;
        void c.offsetWidth;
        c.style.transition = t;
      } else {
        c.style.transform = tf;
      }
      c.style.zIndex = i + 1;
      c.style.opacity = d > 3 ? 0 : 1;
      c.style.filter = `brightness(${1 - d * .12})`;
    });
  }

  function sendBack(card, dir) {
    done++; meter();
    card.style.transform = `translateX(${dir * 480}px) rotate(${dir * 22}deg)`;
    card.style.opacity = '0';
    setTimeout(() => {
      card.style.transition = 'none';
      deck.insertBefore(card, deck.firstElementChild);
      card.style.opacity = '';
      card.style.filter = '';
      const k = card.querySelector('.scd-flash__verdict--know');
      const a = card.querySelector('.scd-flash__verdict--again');
      if (k) k.style.opacity = 0;
      if (a) a.style.opacity = 0;
      layout(true);
      void card.offsetWidth;
      card.style.transition = '';
      if (done >= total) done = 0;
      meter(); bind();
    }, 420);
  }

  let activeTop = null;
  let sx = 0, dx = 0, drag = false, moved = false;
  let know = null, again = null;

  function onDown(e) {
    drag = true; moved = false;
    sx = (e.touches ? e.touches[0].clientX : e.clientX);
    if (activeTop) activeTop.classList.add('scd-flash__card--drag');
  }
  function onMove(e) {
    if (!drag || !activeTop) return;
    dx = (e.touches ? e.touches[0].clientX : e.clientX) - sx;
    if (Math.abs(dx) > 5) moved = true;
    activeTop.style.transform = `translateX(${dx}px) rotate(${dx * .05}deg)`;
    if (know) know.style.opacity = Math.max(0, Math.min(1, dx / 90));
    if (again) again.style.opacity = Math.max(0, Math.min(1, -dx / 90));
  }
  function onUp() {
    if (!drag || !activeTop) return;
    drag = false;
    activeTop.classList.remove('scd-flash__card--drag');
    if (!moved) { sendBack(activeTop, -1); return; }
    if (Math.abs(dx) > 100) {
      sendBack(activeTop, dx > 0 ? 1 : -1);
    } else {
      if (know) know.style.opacity = 0;
      if (again) again.style.opacity = 0;
      layout();
    }
    dx = 0;
  }

  function bind() {
    const cards = [...deck.querySelectorAll('.scd-flash__card')];
    const top = cards[cards.length - 1];
    if (!top) return;
    activeTop = top;
    know = top.querySelector('.scd-flash__verdict--know');
    again = top.querySelector('.scd-flash__verdict--again');
    top.onmousedown = onDown;
    top.ontouchstart = onDown;
  }

  root.addEventListener('mousemove', onMove);
  root.addEventListener('touchmove', onMove, { passive: true });
  root.addEventListener('mouseup', onUp);
  root.addEventListener('touchend', onUp);
  root.addEventListener('mouseleave', onUp);

  stage.addEventListener('mousemove', (e) => {
    stage.querySelectorAll('.scd-flash__card').forEach((c) => {
      const r = c.getBoundingClientRect();
      c.style.setProperty('--mx', ((e.clientX - r.left) / r.width * 100) + '%');
      c.style.setProperty('--my', ((e.clientY - r.top) / r.height * 100) + '%');
    });
  });

  meter(); layout(); bind();
})();