How To Create A Card Matching Game Using React

- Published on

Building a Card Matching Game in React: A Step-by-Step Guide
If you're looking for a fun way to sharpen your React skills, building a memory-based Card Matching Game is a great project. It combines core frontend techniques—state management, component design, effects, and conditional rendering—into a single interactive application.
In this guide, we’ll walk through:
- Designing the structure of the matching game
- Managing state with React hooks (
useState
,useEffect
) - Handling game logic (flipping, matching, resetting)
- Styling the board and cards with CSS
- Ideas for extending the game (scores, timers, difficulty levels)
What We're Building
The core idea: display a grid of cards, all face-down. The user clicks to flip two cards. If they match, they stay visible. If not, they flip back after a short delay. The game ends when all matches are found.
Here’s what we’ll need:
- A deck of card objects (duplicated and shuffled)
- A
Card
component to display each card - A main
GameBoard
to manage state - Logic for handling flips, checking matches, and resetting
Step 1: Set Up the Project
Create a new React app:
npx create-react-app card-matching-game --template typescript
cd card-matching-game
Add Tailwind CSS (optional for styling):
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Update your tailwind.config.js
and include the Tailwind directives in index.css
.
Step 2: Create Card Data
Create a file src/data/cards.ts
:
const cardImages = [
{ src: '/img/helmet.png', matched: false },
{ src: '/img/potion.png', matched: false },
{ src: '/img/ring.png', matched: false },
{ src: '/img/scroll.png', matched: false },
{ src: '/img/shield.png', matched: false },
{ src: '/img/sword.png', matched: false }
]
export default cardImages
🔁 Each image will appear twice in the game deck (for matching).
Step 3: Build the Card Component
Create src/components/Card.tsx
:
interface CardProps {
card: {
src: string
matched: boolean
id: number
}
handleChoice: (card: any) => void
flipped: boolean
disabled: boolean
}
export default function Card({ card, handleChoice, flipped, disabled }: CardProps) {
const handleClick = () => {
if (!disabled) handleChoice(card)
}
return (
<div className="w-24 h-32 cursor-pointer perspective" onClick={handleClick}>
<div className={`relative w-full h-full transition-transform duration-500 ${flipped ? 'rotate-y-180' : ''}`}>
<img
className="absolute w-full h-full backface-hidden"
src={card.src}
alt="card front"
/>
<div className="absolute w-full h-full bg-gray-800 backface-hidden rotate-y-180" />
</div>
</div>
)
}
✨ Styling classes assume Tailwind CSS, but you can adapt to regular CSS or styled-components.
Step 4: Game Logic in App.tsx
import { useEffect, useState } from 'react'
import cardImages from './data/cards'
import Card from './components/Card'
function App() {
const [cards, setCards] = useState<any[]>([])
const [firstChoice, setFirstChoice] = useState<any>(null)
const [secondChoice, setSecondChoice] = useState<any>(null)
const [disabled, setDisabled] = useState(false)
const [turns, setTurns] = useState(0)
const shuffleCards = () => {
const shuffled = [...cardImages, ...cardImages]
.map((card) => ({ ...card, id: Math.random() }))
.sort(() => Math.random() - 0.5)
setFirstChoice(null)
setSecondChoice(null)
setCards(shuffled)
setTurns(0)
}
const handleChoice = (card: any) => {
firstChoice ? setSecondChoice(card) : setFirstChoice(card)
}
useEffect(() => {
if (firstChoice && secondChoice) {
setDisabled(true)
if (firstChoice.src === secondChoice.src) {
setCards((prev) =>
prev.map((card) =>
card.src === firstChoice.src ? { ...card, matched: true } : card
)
)
resetTurn()
} else {
setTimeout(() => resetTurn(), 1000)
}
}
}, [firstChoice, secondChoice])
const resetTurn = () => {
setFirstChoice(null)
setSecondChoice(null)
setTurns((t) => t + 1)
setDisabled(false)
}
useEffect(() => {
shuffleCards()
}, [])
return (
<div className="text-center">
<h1 className="text-2xl font-bold mb-4">Card Matching Game</h1>
<button
onClick={shuffleCards}
className="mb-4 px-4 py-2 bg-indigo-600 text-white rounded"
>
New Game
</button>
<div className="grid grid-cols-4 gap-4 justify-center">
{cards.map((card) => (
<Card
key={card.id}
card={card}
handleChoice={handleChoice}
flipped={card === firstChoice || card === secondChoice || card.matched}
disabled={disabled}
/>
))}
</div>
<p className="mt-4">Turns: {turns}</p>
</div>
)
}
export default App
🧠 Logic Highlights:
- We track two selected cards using state
- If they match,
matched
is set to true- If not, they flip back after 1 second
- A "New Game" button reshuffles and resets the board
Styling Tips
To enhance the visual appeal:
- Add card flip animations using
transform: rotateY(180deg)
- Show a success animation when all pairs are matched
- Add sound effects on flip and match
- Display emojis or text as fallback if images are not used
Advanced Extensions
Want to make it more interactive? Try adding:
- ⏱ Timer and best-time tracker
- 🧠 Difficulty levels (3x4, 4x4, 6x6)
- 🏆 Leaderboard using
localStorage
or a backend - 👨👩👧👦 Multiplayer support with WebSockets or Firebase
- 🔄 Animations with Framer Motion or GSAP
Final Thoughts
This game is a great introduction to:
- Handling stateful components in React
- Using effects to trigger logic
- Structuring reusable components like cards
- Managing game state with multiple variables
You can easily take this foundation and customize it into your own theme—Pokemon, icons, countries, emojis, or even math symbols for educational use.
Resources
- React Docs – Official React documentation
- Tailwind CSS – For styling the game board
- Framer Motion – For animations
- useSound – Add sound to your game