G GAMESRICH
Kids & Toddlers ยท Beginner

How to Build a 3D Card Matching Game: Memori Ceria

Create modern CSS 3D card flips, array shuffling algorithms, and match verification logic.

By GamesRich Engineering 6 min read Zero External Libraries

Play the Live Game First

Experience the mechanics firsthand to understand what you are building.

PLAY LIVE GAME โ†—

Architecture & Core Concept

Unlike canvas games, memory card matching is best built using semantic HTML <button> elements. Each card contains two faces: - .face.front (the hidden picture) - .face.back (the card backing design) CSS transform: rotateY(180deg) flips the card. When two cards are opened: If their dataset.value matches, tag both with .matched and play a celebratory chime. If they mismatch, lock clicks for 800ms, then flip both back.

Key Game Mechanics Taught:

Step-by-Step Implementation

1

CSS 3D Card Flip Structure

Using CSS perspective and transform-style: preserve-3d enables GPU-accelerated 3D flips without complex 3D libraries.

Step 1 Implementation
.card {
  perspective: 800px;
  position: relative;
  width: 100px; height: 120px;
  background: transparent; border: none; cursor: pointer;
}

.card-inner {
  width: 100%; height: 100%;
  position: relative;
  transform-style: preserve-3d;
  transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}

.card.flipped .card-inner {
  transform: rotateY(180deg);
}

.face {
  position: absolute; inset: 0;
  backface-visibility: hidden;
  border-radius: 8px; display: flex; align-items: center; justify-content: center;
}

.face.back { background: #3b82f6; }
.face.front { background: #ffffff; transform: rotateY(180deg); }
2

The Fisher-Yates Shuffle Algorithm

To ensure perfectly random and fair card distribution every round, duplicate the icons and shuffle using the Fisher-Yates swap method.

Step 2 Implementation
function shuffleDeck(items) {
  const deck = [...items, ...items]; // Duplicate for pairs
  for (let i = deck.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [deck[i], deck[j]] = [deck[j], deck[i]];
  }
  return deck;
}
3

Handle Card Comparison & Input Locking

Lock user interactions while checking mismatched cards so the player cannot accidentally flip a third card before the animation completes.

Step 3 Implementation
let firstCard = null, secondCard = null, isLocked = false;

function onCardClick(card) {
  if (isLocked || card === firstCard || card.classList.contains('matched')) return;

  card.classList.add('flipped');

  if (!firstCard) {
    firstCard = card;
    return;
  }

  secondCard = card;
  isLocked = true;

  if (firstCard.dataset.value === secondCard.dataset.value) {
    // Match!
    setTimeout(() => {
      firstCard.classList.add('matched');
      secondCard.classList.add('matched');
      firstCard = secondCard = null;
      isLocked = false;
      playSuccessChime();
    }, 400);
  } else {
    // Mismatch: flip back after 850ms
    setTimeout(() => {
      firstCard.classList.remove('flipped');
      secondCard.classList.remove('flipped');
      firstCard = secondCard = null;
      isLocked = false;
    }, 850);
  }
}

Complete Standalone Source Code

Here is the full, self-contained implementation. Save this as an index.html file on your computer and open it in any web browser to run your game immediately:

Complete Runnable Game (index.html)
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Memory Card Match Demo</title>
<style>
body { margin:0; background:#f0fdf4; display:flex; flex-direction:column; align-items:center; padding:30px; font-family:sans-serif; }
.grid { display:grid; grid-template-columns:repeat(4, 90px); gap:14px; margin-top:20px; }
.card { width:90px; height:110px; perspective:800px; border:none; background:none; cursor:pointer; }
.inner { width:100%; height:100%; position:relative; transform-style:preserve-3d; transition:transform 0.4s; }
.card.flipped .inner { transform:rotateY(180deg); }
.face { position:absolute; inset:0; backface-visibility:hidden; border-radius:10px; display:flex; align-items:center; justify-content:center; font-size:40px; box-shadow:0 4px 10px rgba(0,0,0,0.1); }
.back { background:#10b981; color:#fff; }
.front { background:#fff; transform:rotateY(180deg); }
.card.matched .front { background:#dcfce7; }
</style></head><body>
<h2>Memori Ceria</h2>
<div class="grid" id="board"></div>
<script>
const board = document.querySelector('#board');
const icons = ['๐ŸŽ','๐Ÿ“','๐ŸŒ','๐Ÿ‰','๐ŸŠ','๐Ÿ‡'];
const deck = [...icons, ...icons].sort(() => Math.random() - 0.5);

let c1 = null, c2 = null, locked = false;
deck.forEach(val => {
  const card = document.createElement('button');
  card.className = 'card';
  card.innerHTML = `<div class="inner"><div class="face back">?</div><div class="face front">${val}</div></div>`;
  card.onclick = () => {
    if(locked || card === c1 || card.classList.contains('matched')) return;
    card.classList.add('flipped');
    if(!c1) { c1 = card; return; }
    c2 = card; locked = true;
    if(c1.querySelector('.front').textContent === c2.querySelector('.front').textContent) {
      setTimeout(() => { c1.classList.add('matched'); c2.classList.add('matched'); c1 = c2 = null; locked = false; }, 350);
    } else {
      setTimeout(() => { c1.classList.remove('flipped'); c2.classList.remove('flipped'); c1 = c2 = null; locked = false; }, 800);
    }
  };
  board.append(card);
});
</script>
</body></html>

Ideas to Expand Your Game

Now that you have the core mechanics running, consider adding: