JSFiddle - React, Tailwind, and code Playground

HTML

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>🧠 Memory Game in JavaScript</title>

        <link rel="stylesheet" href="assets/styles.css" />
        <script src="assets/game.js" defer></script>
    </head>
    <body>
        <div class="game">
            <div class="controls">
                <button>Start</button>
                <div class="stats">
                    <div class="moves">0 moves</div>
                    <div class="timer">time: 0 sec</div>
                </div>
            </div>
            <div class="board-container">
                <div class="board" data-dimension="4"></div>
                <div class="win">You won!</div>
            </div>
        </div>
    </body>
</html>

CSS

@font-face {
    font-family: Fredoka;
    src: url(./FredokaOne-Regular.ttf);
}

html {
    width: 100%;
    height: 100%;
    background: linear-gradient(325deg,  #6f00fc 0%,#fc7900 50%,#fcc700 100%);
    font-family: Fredoka;
}

.game {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

.controls {
    display: flex;
    gap: 20px;
    margin-bottom: 20px;
}

button {
    background: #282A3A;
    color: #FFF;
    border-radius: 5px;
    padding: 10px 20px;
    border: 0;
    cursor: pointer;
    font-family: Fredoka;
    font-size: 18pt;
}

.disabled {
    color: #757575;
}

.stats {
    color: #FFF;
    font-size: 14pt;
}

.board-container {
    position: relative;
}

.board,
.win {
    border-radius: 5px;
    box-shadow: 0 25px 50px rgb(33 33 33 / 25%);
    background: linear-gradient(135deg,  #6f00fc 0%,#fc7900 50%,#fcc700 100%);
    transition: transform .6s cubic-bezier(0.4, 0.0, 0.2, 1);
    backface-visibility: hidden;
}

.board {
    padding: 20px;
    display: grid;
    grid-template-columns: repeat(4, auto);
    grid-gap: 20px;
}

.board-container.flipped .board {
    transform: rotateY(180deg) rotateZ(50deg);
}

.board-container.flipped .win {
    transform: rotateY(0) rotateZ(0);
}

.card {
    position: relative;
    width: 100px;
    height: 100px;
    cursor: pointer;
}

.card-front,
.card-back {
    position: absolute;
    border-radius: 5px;
    width: 100%;
    height: 100%;
    background: #282A3A;
    transition: transform .6s cubic-bezier(0.4, 0.0, 0.2, 1);
    backface-visibility: hidden;
}

.card-back {
    transform: rotateY(180deg) rotateZ(50deg);
    font-size: 28pt;
    user-select: none;
    text-align: center;
    line-height: 100px;
    background: #FDF8E6;
}

.card.flipped .card-front {
    transform: rotateY(180deg) rotateZ(50deg);
}

.card.flipped .card-back {
    transform: rotateY(0) rotateZ(0);
}

.win {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height:...

JavaScript

const selectors = {
    boardContainer: document.querySelector('.board-container'),
    board: document.querySelector('.board'),
    moves: document.querySelector('.moves'),
    timer: document.querySelector('.timer'),
    start: document.querySelector('button'),
    win: document.querySelector('.win')
}

const state = {
    gameStarted: false,
    flippedCards: 0,
    totalFlips: 0,
    totalTime: 0,
    loop: null
}

const shuffle = array => {
    const clonedArray = [...array]

    for (let index = clonedArray.length - 1; index > 0; index--) {
        const randomIndex = Math.floor(Math.random() * (index + 1))
        const original = clonedArray[index]

        clonedArray[index] = clonedArray[randomIndex]
        clonedArray[randomIndex] = original
    }

    return clonedArray
}

const pickRandom = (array, items) => {
    const clonedArray = [...array]
    const randomPicks = []

    for (let index = 0; index < items; index++) {
        const randomIndex = Math.floor(Math.random() * clonedArray.length)
        
        randomPicks.push(clonedArray[randomIndex])
        clonedArray.splice(randomIndex, 1)
    }

    return randomPicks
}

const generateGame = () => {
    const dimensions = selectors.board.getAttribute('data-dimension')

    if (dimensions % 2 !== 0) {
        throw new Error("The dimension of the board must be an even number.")
    }

    const emojis = ['πŸ₯”', 'πŸ’', 'πŸ₯‘', '🌽', 'πŸ₯•', 'πŸ‡', 'πŸ‰', '🍌', 'πŸ₯­', '🍍']
    const picks = pickRandom(emojis, (dimensions * dimensions) / 2) 
    const items = shuffle([...picks, ...picks])
    const cards = `
        <div class="board" style="grid-template-columns: repeat(${dimensions}, auto)">
            ${items.map(item => `
                <div class="card">
                    <div class="card-front"></div>
                    <div class="card-back">${item}</div>
                </div>
            `).join('')}
       </div>
    `
    
    const parser = new DOMParser().parseFromString(cards, 'text/html')

   ...