Match Maker!
Memory Card Game
No canvas this time, build a card-flipping memory game out of real HTML elements, CSS 3D flips and DOM events. This is the same skill set as building web apps, disguised as a game.
No canvas this time, build a card-flipping memory game out of real HTML elements, CSS 3D flips and DOM events. This is the same skill set as building web apps, disguised as a game.
Build a 4ร4 grid of cards entirely from JavaScript.
<div id="board"></div>
<style>
#board { display: grid; grid-template-columns: repeat(4, 90px); gap: 10px; }
.card { height: 90px; background: #2a2d3a; border-radius: 12px;
display: flex; align-items: center; justify-content: center;
font-size: 2rem; cursor: pointer; color: #fff; }
</style>
<script>
const EMOJI = ["๐","๐","๐","๐ฎ","๐ฑ","โก","๐","๐ฒ"];
let values = [...EMOJI, ...EMOJI]; // 8 pairs = 16 cards
const board = document.getElementById("board");
for (const v of values) {
const card = document.createElement("div");
card.className = "card";
card.dataset.value = v;
card.textContent = "?";
board.appendChild(card);
}
</script><div id="board"></div>
<style>
#board { display: grid; grid-template-columns: repeat(4, 90px); gap: 10px; }
.card { height: 90px; background: #2a2d3a; border-radius: 12px;
display: flex; align-items: center; justify-content: center;
font-size: 2rem; cursor: pointer; color: #fff; }
</style>
<script>
const EMOJI = ["๐","๐","๐","๐ฎ","๐ฑ","โก","๐","๐ฒ"];
let values = [...EMOJI, ...EMOJI]; // 8 pairs = 16 cards
const board = document.getElementById("board");
for (const v of values) {
const card = document.createElement("div");
card.className = "card";
card.dataset.value = v;
card.textContent = "?";
board.appendChild(card);
}
</script>
Randomise card order with the classic fair-shuffle algorithm.
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]]; // swap!
}
return arr;
}
values = shuffle(values);function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]]; // swap!
}
return arr;
}
values = shuffle(values);
for (let i = arr.length - 1; i > for (let i = arr.length - 1; i > ; i--) {
const j = Math.floor(Math.random() * (i + ; i--) {
const j = Math.floor(Math.random() * (i + ));
[arr[i], arr[j]] = [arr));
[arr[i], arr[j]] = [arr], arr[i]];
}], arr[i]];
}
Flip cards over with a smooth CSS 3D rotation.
<style>
.card { transition: transform 0.35s; }
.card.flipped { transform: rotateY(180deg); background: #ffd166; color: #222; }
</style>
<script>
board.addEventListener("click", function(e) {
const card = e.target.closest(".card");
if (!card || card.classList.contains("flipped")) return;
card.classList.add("flipped");
card.textContent = card.dataset.value;
});
</script><style>
.card { transition: transform 0.35s; }
.card.flipped { transform: rotateY(180deg); background: #ffd166; color: #222; }
</style>
<script>
board.addEventListener("click", function(e) {
const card = e.target.closest(".card");
if (!card || card.classList.contains("flipped")) return;
card.classList.add("flipped");
card.textContent = card.dataset.value;
});
</script>
Compare pairs and lock matches or flip mismatches back.
let first = null, locked = false;
function onCardClick(card) {
if (locked || card.classList.contains("flipped")) return;
flip(card);
if (!first) { first = card; return; }
if (first.dataset.value === card.dataset.value) {
first.classList.add("matched");
card.classList.add("matched");
first = null;
} else {
locked = true;
setTimeout(function() {
unflip(first); unflip(card);
first = null;
locked = false;
}, 800);
}
}let first = null, locked = false;
function onCardClick(card) {
if (locked || card.classList.contains("flipped")) return;
flip(card);
if (!first) { first = card; return; }
if (first.dataset.value === card.dataset.value) {
first.classList.add("matched");
card.classList.add("matched");
first = null;
} else {
locked = true;
setTimeout(function() {
unflip(first); unflip(card);
first = null;
locked = false;
}, 800);
}
}
locked = locked = ;
;
(function() {
unflip(first); unflip(card);
first = null;
locked = (function() {
unflip(first); unflip(card);
first = null;
locked = ;
}, 800);;
}, 800);
Add a move counter, a live timer and the win banner.
let moves = 0, seconds = 0, timerId = null;
function startTimer() {
if (timerId) return; // only once
timerId = setInterval(function() {
seconds++;
document.getElementById("time").textContent = seconds + "s";
}, 1000);
}
function checkWin() {
if (document.querySelectorAll(".matched").length === 16) {
clearInterval(timerId);
document.getElementById("msg").textContent =
"๐ You won in " + moves + " moves and " + seconds + "s!";
}
}let moves = 0, seconds = 0, timerId = null;
function startTimer() {
if (timerId) return; // only once
timerId = setInterval(function() {
seconds++;
document.getElementById("time").textContent = seconds + "s";
}, 1000);
}
function checkWin() {
if (document.querySelectorAll(".matched").length === 16) {
clearInterval(timerId);
document.getElementById("msg").textContent =
"๐ You won in " + moves + " moves and " + seconds + "s!";
}
}
Add difficulty levels and persistent best scores per level.
function newGame(size) { // 4 or 6
board.style.gridTemplateColumns =
"repeat(" + size + ", 90px)";
const pairs = (size * size) / 2;
values = shuffle([...EMOJI.slice(0, pairs),
...EMOJI.slice(0, pairs)]);
buildBoard(values);
moves = 0; seconds = 0;
}
// after a win:
const key = "memory-best-" + size;
const best = Number(localStorage.getItem(key) || 999);
if (moves < best) localStorage.setItem(key, moves);function newGame(size) { // 4 or 6
board.style.gridTemplateColumns =
"repeat(" + size + ", 90px)";
const pairs = (size * size) / 2;
values = shuffle([...EMOJI.slice(0, pairs),
...EMOJI.slice(0, pairs)]);
buildBoard(values);
moves = 0; seconds = 0;
}
// after a win:
const key = "memory-best-" + size;
const best = Number(localStorage.getItem(key) || 999);
if (moves < best) localStorage.setItem(key, moves);
const pairs = (size * const pairs = (size * ) / ) / ;;
A self-building card grid, a fair Fisher-Yates shuffle, CSS flip animation, match logic with the lock-during-wait fix, a move counter and timer, a win banner, plus Easy/Hard difficulty and best scores in localStorage , one file. Save it as memory.htmlmemory.html, open it, and play. Stuck on a step? Compare your file against this.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Memory Match</title>
<style>
body { background:#0d1117; color:#e8eaf2; font-family:system-ui, sans-serif;
display:flex; flex-direction:column; align-items:center; gap:12px; padding:16px; }
#hud { display:flex; gap:16px; align-items:center; flex-wrap:wrap; justify-content:center; }
button { background:#2a2d3a; color:#fff; border:1px solid #3a3f52; border-radius:10px;
padding:8px 14px; font-size:.9rem; cursor:pointer; }
button:hover { background:#3a3f52; }
#board { display:grid; gap:10px; justify-content:center; margin:8px auto; }
.card { height:90px; width:90px; background:#2a2d3a; border-radius:12px;
display:flex; align-items:center; justify-content:center;
font-size:2rem; cursor:pointer; color:#fff; transition:transform .35s; }
.card.flipped { transform:rotateY(180deg); background:#ffd166; color:#222; }
.card.matched { background:#06d6a0; color:#08221b; cursor:default; }
#msg { font-size:1.1rem; min-height:1.4em; }
</style>
</head>
<body>
<h2>๐ง Memory Match</h2>
<div id="hud">
<button onclick="newGame(4)">Easy 4ร4</button>
<button onclick="newGame(6)">Hard 6ร6</button>
<span>Moves: <b id="moves">0</b></span>
<span>Time: <b id="time">0s</b></span>
<span id="best"></span>
</div>
<div id="board"></div>
<div id="msg"></div>
<script>
// 18 emoji so Hard 6x6 (18 pairs) works; Easy 4x4 uses the first 8
const EMOJI = ["๐","๐","๐","๐ฎ","๐ฑ","โก","๐","๐ฒ",
"๐ถ","๐","๐","๐ธ","๐ง","๐ฅ","๐","๐ฏ","๐","๐"];
const board = document.getElementById("board");
let values = [], first = null, locked = false, moves = 0, seconds = 0, timerId = null, size = 4;
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]]; // one-line swap
}
return arr;
}
function buildBoard(vals) {
board.innerHTML = "";
board.style.gridTemplateColumns = "repeat(" + size + ", 90px)";
for (const v of vals) {
const card = document.createElement("div");
card.className = "card";
card.dataset.value = v;
card.textContent = "?";
board.appendChild(card);
}
}
function flip(card) { card.classList.add("flipped"); card.textContent = card.dataset.value; }
function unflip(card) { card.classList.remove("flipped"); card.textContent = "?"; }
function startTimer() {
if (timerId) return;
timerId = setInterval(function () {
seconds++;
document.getElementById("time").textContent = seconds + "s";
}, 1000);
}
function showBest() {
const b4 = localStorage.getItem("memory-best-4") || "โ";
const b6 = localStorage.getItem("memory-best-6") || "โ";
document.getElementById("best").textContent = "Best: 4ร4 " + b4 + " ยท 6ร6 " + b6;
}
function checkWin() {
if (document.querySelectorAll(".matched").length === size * size) {
clearInterval(timerId); timerId = null;
const key = "memory-best-" + size;
const best = Number(localStorage.getItem(key) || 999);
if (moves < best) localStorage.setItem(key, moves);
document.getElementById("msg").textContent = "๐ You won in " + moves + " moves and " + seconds + "s!";
showBest();
}
}
function onCardClick(card) {
if (locked || card.classList.contains("flipped") || card.classList.contains("matched")) return;
startTimer();
flip(card);
if (!first) { first = card; return; }
moves++;
document.getElementById("moves").textContent = moves;
if (first.dataset.value === card.dataset.value) {
first.classList.add("matched");
card.classList.add("matched");
first = null;
checkWin();
} else {
locked = true;
setTimeout(function () {
unflip(first); unflip(card);
first = null; locked = false;
}, 800);
}
}
board.addEventListener("click", function (e) {
const card = e.target.closest(".card");
if (card) onCardClick(card);
});
function newGame(n) {
size = n;
const pairs = (size * size) / 2;
values = shuffle([...EMOJI.slice(0, pairs), ...EMOJI.slice(0, pairs)]);
buildBoard(values);
moves = 0; seconds = 0; first = null; locked = false;
clearInterval(timerId); timerId = null;
document.getElementById("moves").textContent = "0";
document.getElementById("time").textContent = "0s";
document.getElementById("msg").textContent = "";
showBest();
}
newGame(4);
</script>
</body>
</html><!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Memory Match</title>
<style>
body { background:#0d1117; color:#e8eaf2; font-family:system-ui, sans-serif;
display:flex; flex-direction:column; align-items:center; gap:12px; padding:16px; }
#hud { display:flex; gap:16px; align-items:center; flex-wrap:wrap; justify-content:center; }
button { background:#2a2d3a; color:#fff; border:1px solid #3a3f52; border-radius:10px;
padding:8px 14px; font-size:.9rem; cursor:pointer; }
button:hover { background:#3a3f52; }
#board { display:grid; gap:10px; justify-content:center; margin:8px auto; }
.card { height:90px; width:90px; background:#2a2d3a; border-radius:12px;
display:flex; align-items:center; justify-content:center;
font-size:2rem; cursor:pointer; color:#fff; transition:transform .35s; }
.card.flipped { transform:rotateY(180deg); background:#ffd166; color:#222; }
.card.matched { background:#06d6a0; color:#08221b; cursor:default; }
#msg { font-size:1.1rem; min-height:1.4em; }
</style>
</head>
<body>
<h2>๐ง Memory Match</h2>
<div id="hud">
<button onclick="newGame(4)">Easy 4ร4</button>
<button onclick="newGame(6)">Hard 6ร6</button>
<span>Moves: <b id="moves">0</b></span>
<span>Time: <b id="time">0s</b></span>
<span id="best"></span>
</div>
<div id="board"></div>
<div id="msg"></div>
<script>
// 18 emoji so Hard 6x6 (18 pairs) works; Easy 4x4 uses the first 8
const EMOJI = ["๐","๐","๐","๐ฎ","๐ฑ","โก","๐","๐ฒ",
"๐ถ","๐","๐","๐ธ","๐ง","๐ฅ","๐","๐ฏ","๐","๐"];
const board = document.getElementById("board");
let values = [], first = null, locked = false, moves = 0, seconds = 0, timerId = null, size = 4;
function shuffle(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]]; // one-line swap
}
return arr;
}
function buildBoard(vals) {
board.innerHTML = "";
board.style.gridTemplateColumns = "repeat(" + size + ", 90px)";
for (const v of vals) {
const card = document.createElement("div");
card.className = "card";
card.dataset.value = v;
card.textContent = "?";
board.appendChild(card);
}
}
function flip(card) { card.classList.add("flipped"); card.textContent = card.dataset.value; }
function unflip(card) { card.classList.remove("flipped"); card.textContent = "?"; }
function startTimer() {
if (timerId) return;
timerId = setInterval(function () {
seconds++;
document.getElementById("time").textContent = seconds + "s";
}, 1000);
}
function showBest() {
const b4 = localStorage.getItem("memory-best-4") || "โ";
const b6 = localStorage.getItem("memory-best-6") || "โ";
document.getElementById("best").textContent = "Best: 4ร4 " + b4 + " ยท 6ร6 " + b6;
}
function checkWin() {
if (document.querySelectorAll(".matched").length === size * size) {
clearInterval(timerId); timerId = null;
const key = "memory-best-" + size;
const best = Number(localStorage.getItem(key) || 999);
if (moves < best) localStorage.setItem(key, moves);
document.getElementById("msg").textContent = "๐ You won in " + moves + " moves and " + seconds + "s!";
showBest();
}
}
function onCardClick(card) {
if (locked || card.classList.contains("flipped") || card.classList.contains("matched")) return;
startTimer();
flip(card);
if (!first) { first = card; return; }
moves++;
document.getElementById("moves").textContent = moves;
if (first.dataset.value === card.dataset.value) {
first.classList.add("matched");
card.classList.add("matched");
first = null;
checkWin();
} else {
locked = true;
setTimeout(function () {
unflip(first); unflip(card);
first = null; locked = false;
}, 800);
}
}
board.addEventListener("click", function (e) {
const card = e.target.closest(".card");
if (card) onCardClick(card);
});
function newGame(n) {
size = n;
const pairs = (size * size) / 2;
values = shuffle([...EMOJI.slice(0, pairs), ...EMOJI.slice(0, pairs)]);
buildBoard(values);
moves = 0; seconds = 0; first = null; locked = false;
clearInterval(timerId); timerId = null;
document.getElementById("moves").textContent = "0";
document.getElementById("time").textContent = "0s";
document.getElementById("msg").textContent = "";
showBest();
}
newGame(4);
</script>
</body>
</html>