<div id="app"></div>
.board {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(60px, 1fr));
  gap: 8px; /* space between cards */
  max-width: 420px; /* optional, center on larger screens */
  margin: 0 auto;
}
/* Individual card button */
.card,
.card-matched {
  aspect-ratio: 1 / 1; /* keep square */
  font-size: 1.25rem;
  border: 1px solid #333;
  border-radius: 6px;
  background: #f5f5f5;
  transition: transform 0.2s;
}

.card:hover:not(:disabled) {
  transform: scale(1.05);
}

.card-matched {
  visibility: hidden; /* hide matched cards (keeps grid gap) */
}
/*
 * https://frontendeval.com/questions/memory-game
 *
 * Create a card matching game to test the player's memory
 */

function shuffle(array) {
  return array.sort(() => Math.random() - 0.5);
}

function getInitDeck() {
  const set = Array.from({ length: 18 })
    .fill(0)
    .map((_, i) => i);
  return shuffle([...set, ...set]).map((value) => ({
    value,
    hidden: true,
    matched: false
  }));
}
const App = () => {
  const [cards, setCards] = React.useState(getInitDeck());
  const [first, setFirst] = React.useState(null);
  const [second, setSecond] = React.useState(null);

  const won = cards.every((c) => c.matched);

  React.useEffect(() => {
    let timeoutId;
    if (first === null || second === null) return;

    const isMatch = cards[first].value === cards[second].value;

    if (isMatch) {
      setCards((prev) =>
        prev.map((c, i) =>
          i === first || i === second ? { ...c, matched: true } : c
        )
      );
      setFirst(null);
      setSecond(null);
      return;
    } else {
      timeoutId = setTimeout(() => {
        setCards((prev) =>
          prev.map((c, i) =>
            i === first || i === second ? { ...c, hidden: true } : c
          )
        );
        setFirst(null);
        setSecond(null);
      }, 3000);
    }

    return () => clearTimeout(timeoutId);
  }, [first, second]);

  function reveal(index) {
    setCards((prev) =>
      prev.map((c, i) => (i === index ? { ...c, hidden: false } : c))
    );
  }

  function handleClick(index) {
    // Dont reveal if
    if (cards[index].matched || !cards[index].hidden || second !== null) return;
    reveal(index);
    first === null ? setFirst(index) : setSecond(index);
  }

  function startOver() {
    setCards(getInitDeck());
    setFirst(null);
    setSecond(null);
  }

  return (
    <div>
      {won ? (
        <>
          <p className="win">You won 🎉</p>
          <button onClick={startOver}>Start over</button>
        </>
      ) : (
        <div className="board">
          {cards.map(({ value, hidden, matched }, i) => (
            <button
              key={i}
              aria-label={hidden ? "hidden card" : String(value)}
              className={`card${matched ? "-matched" : ""}`}
              onClick={() => handleClick(i)}
              disabled={(first && second) || matched}
            >
              {!hidden && !matched && value}
            </button>
          ))}
        </div>
      )}
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("app"));
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js