Back to CSS Stacked Cards Scratch Cards CSS + JS
Share
HTML
<div class="scd-scratch">
  <div class="scd-scratch__stage">
    <div class="scd-scratch__scratch scd-scratch__scratch--s3"><div class="scd-scratch__prize"><span class="scd-scratch__lbl">PRIZE</span><span class="scd-scratch__amt">$5</span></div></div>
    <div class="scd-scratch__scratch scd-scratch__scratch--s2"><div class="scd-scratch__prize"><span class="scd-scratch__lbl">PRIZE</span><span class="scd-scratch__amt">$20</span></div></div>
    <div class="scd-scratch__scratch scd-scratch__scratch--s1"><div class="scd-scratch__prize"><span class="scd-scratch__lbl">WINNER</span><span class="scd-scratch__amt">$100</span></div><canvas data-scd-scratch="canvas"></canvas></div>
  </div>
</div>
CSS
@import url('https://fonts.googleapis.com/css2?family=Bungee&family=Nunito:wght@600;800&display=swap');

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

.scd-scratch {
  min-height: 460px;
  display: grid;
  place-items: center;
  background: linear-gradient(135deg,#f12711,#f5af19);
  font-family: 'Nunito', sans-serif;
}
.scd-scratch__stage { position: relative; width: 240px; height: 320px; }
.scd-scratch__scratch {
  position: absolute; inset: 0;
  border-radius: 20px;
  box-shadow: 0 14px 30px rgba(120,40,0,.4);
  transition: transform .5s cubic-bezier(.3,.9,.3,1);
  overflow: hidden;
}
.scd-scratch__prize {
  position: absolute; inset: 0;
  background: linear-gradient(150deg,#fffbe6,#ffe9a8);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  color: #c0392b;
}
.scd-scratch__amt { font-family: 'Bungee', sans-serif; font-size: 3rem; }
.scd-scratch__lbl { font-weight: 800; letter-spacing: .1em; color: #e67e22; }
.scd-scratch__scratch canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border-radius: 20px;
  cursor: grab;
  touch-action: none;
}
.scd-scratch__scratch--s1 { z-index: 3; }
.scd-scratch__scratch--s2 { z-index: 2; transform: translate(10px,12px) scale(.96) rotate(3deg); }
.scd-scratch__scratch--s3 { z-index: 1; transform: translate(20px,24px) scale(.92) rotate(6deg); }
.scd-scratch__stage:hover .scd-scratch__scratch--s2 { transform: translate(-110px,18px) scale(.96) rotate(-6deg); }
.scd-scratch__stage:hover .scd-scratch__scratch--s3 { transform: translate(120px,18px) scale(.96) rotate(8deg); }

@media (prefers-reduced-motion: reduce) {
  .scd-scratch__scratch { transition: none !important; }
}
JS
(() => {
  const root = document.querySelector('.scd-scratch');
  if (!root) return;
  const cv = root.querySelector('[data-scd-scratch="canvas"]');
  if (!cv) return;
  const ctx = cv.getContext('2d');

  function init() {
    cv.width = 240;
    cv.height = 320;
    const g = ctx.createLinearGradient(0, 0, 240, 320);
    g.addColorStop(0, '#9e9e9e');
    g.addColorStop(1, '#cfcfcf');
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, 240, 320);
    ctx.fillStyle = '#7a7a7a';
    ctx.font = 'bold 22px Nunito';
    ctx.textAlign = 'center';
    ctx.fillText('SCRATCH HERE', 120, 165);
    ctx.globalCompositeOperation = 'destination-out';
  }
  init();

  let drawing = false;
  function pos(e) {
    const r = cv.getBoundingClientRect();
    const cx = (e.touches ? e.touches[0].clientX : e.clientX) - r.left;
    const cy = (e.touches ? e.touches[0].clientY : e.clientY) - r.top;
    return [cx * 240 / r.width, cy * 320 / r.height];
  }
  function scratch(e) {
    if (!drawing) return;
    const [x, y] = pos(e);
    ctx.beginPath();
    ctx.arc(x, y, 22, 0, 7);
    ctx.fill();
  }
  cv.addEventListener('mousedown', () => { drawing = true; });
  cv.addEventListener('mouseup', () => { drawing = false; });
  cv.addEventListener('mouseleave', () => { drawing = false; });
  cv.addEventListener('mousemove', scratch);
  cv.addEventListener('touchstart', () => { drawing = true; });
  cv.addEventListener('touchend', () => { drawing = false; });
  cv.addEventListener('touchmove', (e) => { e.preventDefault(); scratch(e); }, { passive: false });
})();