Wordle. The daily word game that has taken the internet by storm. If you’re reading this, chances are you’ve played it, maybe even become obsessed with it. But have you ever wondered how it’s built? In this tutorial, we’re going to dive into the world of Next.js and create our very own Wordle clone. This project is perfect for beginners and intermediate developers looking to sharpen their skills with React, state management, and API interactions. We’ll break down each step, explaining the concepts in a clear, concise manner, and provide plenty of code examples.
Why Build a Wordle Clone?
Building a Wordle clone isn’t just a fun project; it’s also a fantastic learning opportunity. It allows you to:
- Master React Components: You’ll create reusable UI elements, a cornerstone of React development.
- Grasp State Management: You’ll learn how to manage the game’s state (guesses, results, etc.) effectively.
- Understand Event Handling: You’ll handle user input and interactions with the game board.
- Work with APIs: You’ll fetch a word for the day, enhancing the game’s dynamic nature.
- Improve UI/UX Skills: You’ll focus on creating a user-friendly and engaging game interface.
Furthermore, it’s a project that’s easily shareable and demonstrates your ability to build interactive web applications.
Prerequisites
Before we begin, make sure you have the following installed:
- Node.js and npm (or yarn): These are essential for running JavaScript and managing project dependencies.
- A code editor: VS Code, Sublime Text, or any editor you’re comfortable with.
- Basic knowledge of JavaScript and React: Familiarity with components, props, and state will be helpful.
Setting Up the Next.js Project
Let’s start by creating a new Next.js project. Open your terminal and run the following command:
npx create-next-app wordle-clone
This command sets up a basic Next.js application. Navigate into the project directory:
cd wordle-clone
Now, let’s install some dependencies we’ll need for this project. In your terminal, run:
npm install react-confetti
This will install the react-confetti package. We’ll use this to display confetti when the player wins.
Project Structure
Let’s take a quick look at the project structure. Next.js has a specific file structure that we’ll be working within.
- pages/: This directory contains your application’s pages. Each file in this directory represents a route (e.g.,
pages/index.jsmaps to the root route,/). - components/: This is where we’ll store reusable React components.
- styles/: This directory is for your CSS or other styling files.
- public/: This directory is for static assets like images.
Building the Game Board Component
The game board is the heart of our Wordle clone. It displays the grid where the player enters their guesses and the results of those guesses. Let’s create a new component for this. Create a file named GameBoard.js inside the components directory.
Here’s the basic structure of the GameBoard.js component:
import React from 'react';
function GameBoard({
guesses,
currentGuess,
wordOfTheDay,
gameStatus,
handleEnter,
}) {
const grid = [];
for (let row = 0; row < 6; row++) {
const rowSquares = [];
for (let col = 0; col < 5; col++) {
let letter = '';
let status = '';
if (row < guesses.length) {
letter = guesses[row][col];
status = getLetterStatus(letter, col, row, wordOfTheDay, guesses);
} else if (row === guesses.length) {
letter = currentGuess[col] || '';
}
rowSquares.push(
<div key={col} className={`square ${status}`}>
{letter.toUpperCase()}
</div>
);
}
grid.push(
<div key={row} className="row">{rowSquares}</div>
);
}
return <div className="game-board">{grid}</div>;
}
function getLetterStatus(letter, col, row, wordOfTheDay, guesses) {
if (!letter) return '';
const correctWord = wordOfTheDay.toUpperCase();
const guess = guesses[row].join('').toUpperCase();
if (correctWord[col] === letter.toUpperCase()) {
return 'correct';
}
if (correctWord.includes(letter.toUpperCase())) {
return 'present';
}
return 'absent';
}
export default GameBoard;
Let’s break down this code:
- Props: The
GameBoardcomponent receives several props: guesses: An array of arrays, representing the player’s previous guesses.currentGuess: An array of characters representing the current guess being typed.wordOfTheDay: The secret word the player is trying to guess.gameStatus: The current state of the game (playing, won, lost).handleEnter: A function to handle the ‘Enter’ key press.- Grid Generation: We use nested loops to create the 6×5 grid of squares.
- Letter Display: We display the letters entered by the player, or empty squares if no letter is entered.
- Styling Classes: We dynamically apply CSS classes (
correct,present,absent) to each square based on the guess’s accuracy. - `getLetterStatus` function: This function checks each letter against the word of the day to determine the status (correct, present, or absent).
Now, let’s add some basic styling to styles/globals.css:
.game-board {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 20px;
}
.row {
display: flex;
margin-bottom: 5px;
}
.square {
width: 50px;
height: 50px;
border: 1px solid #ddd;
display: flex;
justify-content: center;
align-items: center;
font-size: 2rem;
font-weight: bold;
margin-right: 5px;
text-transform: uppercase;
}
.square.correct {
background-color: #6aaa64;
color: white;
border: none;
}
.square.present {
background-color: #c9b458;
color: white;
border: none;
}
.square.absent {
background-color: #787c7e;
color: white;
border: none;
}
Creating the Keyboard Component
The keyboard is another crucial component, allowing the player to input their guesses. Create a new file named Keyboard.js inside the components directory.
import React from 'react';
function Keyboard({
onKeyPress,
guesses,
wordOfTheDay,
gameStatus,
handleEnter,
handleDelete,
}) {
const keyboardRows = [
['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],
['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'],
['Z', 'X', 'C', 'V', 'B', 'N', 'M', '<'],
];
const getLetterStatus = (letter) => {
if (gameStatus !== 'playing') return '';
const correctWord = wordOfTheDay.toUpperCase();
let status = '';
guesses.forEach((guess) => {
if (guess.join('').toUpperCase().includes(letter.toUpperCase())) {
status = 'present';
}
if (correctWord.includes(letter.toUpperCase())) {
status = 'present';
}
if (correctWord.includes(letter.toUpperCase()) && correctWord.split('').filter(char => char === letter.toUpperCase()).length > guess.filter(char => char === letter.toUpperCase()).length) {
status = 'present';
}
if (guess.join('').toUpperCase().indexOf(letter.toUpperCase()) === correctWord.indexOf(letter.toUpperCase())) {
status = 'correct';
}
});
return status;
};
return (
<div className="keyboard">
{keyboardRows.map((row, index) => (
<div key={index} className="keyboard-row">
{row.map((letter) => (
<button
key={letter}
className={`key ${getLetterStatus(letter)}`}
onClick={() => {
if (letter === '<') {
handleDelete();
} else if (letter === 'Enter') {
handleEnter();
} else {
onKeyPress(letter);
}
}}
disabled={gameStatus !== 'playing'}
>
{letter === '<' ? 'Delete' : letter}
</button>
))}
</div>
))}
</div>
);
}
export default Keyboard;
Let’s break down this code:
- Keyboard Layout: We define the keyboard layout in the
keyboardRowsarray. - Props: The
Keyboardcomponent receives several props: onKeyPress: A function to handle key presses.guesses: An array of arrays, representing the player’s previous guesses.wordOfTheDay: The secret word the player is trying to guess.gameStatus: The current state of the game (playing, won, lost).handleEnter: A function to handle the ‘Enter’ key press.handleDelete: A function to handle the ‘Delete’ key press.- Key Press Handling: Each key has an
onClickhandler that calls theonKeyPressfunction (for letter keys) orhandleEnterandhandleDeletefor special keys. - Styling Classes: We dynamically apply CSS classes (
correct,present,absent) to each key based on the guess’s accuracy.
Let’s add some basic styling to styles/globals.css:
.keyboard {
margin-top: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.keyboard-row {
display: flex;
margin-bottom: 5px;
}
.key {
width: 30px;
height: 40px;
border: 1px solid #ddd;
margin: 2px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
background-color: #fff;
color: black;
border-radius: 4px;
}
.key.correct {
background-color: #6aaa64;
color: white;
border: none;
}
.key.present {
background-color: #c9b458;
color: white;
border: none;
}
.key.absent {
background-color: #787c7e;
color: white;
border: none;
}
.key:disabled {
opacity: 0.5;
cursor: not-allowed;
}
Building the Main App Component (pages/index.js)
Now, let’s put everything together in our main app component, located in pages/index.js.
import React, { useState, useEffect } from 'react';
import Head from 'next/head';
import GameBoard from '../components/GameBoard';
import Keyboard from '../components/Keyboard';
import Confetti from 'react-confetti';
function App() {
const [wordOfTheDay, setWordOfTheDay] = useState('');
const [guesses, setGuesses] = useState([]);
const [currentGuess, setCurrentGuess] = useState([]);
const [gameStatus, setGameStatus] = useState('playing'); // playing, won, lost
const [confettiActive, setConfettiActive] = useState(false);
useEffect(() => {
const fetchWord = async () => {
try {
const response = await fetch('/api/word');
const data = await response.json();
setWordOfTheDay(data.word);
} catch (error) {
console.error('Error fetching word:', error);
// Handle error (e.g., set a default word or display an error message)
setWordOfTheDay('REACT'); // Default word if fetching fails
}
};
fetchWord();
}, []);
useEffect(() => {
if (gameStatus === 'won') {
setConfettiActive(true);
setTimeout(() => setConfettiActive(false), 3000);
}
}, [gameStatus]);
const handleKeyPress = (key) => {
if (currentGuess.length < 5 && gameStatus === 'playing') {
setCurrentGuess([...currentGuess, key]);
}
};
const handleDelete = () => {
if (currentGuess.length > 0 && gameStatus === 'playing') {
setCurrentGuess(currentGuess.slice(0, -1));
}
};
const handleEnter = () => {
if (currentGuess.length !== 5 || gameStatus !== 'playing') return;
const newGuesses = [...guesses, currentGuess];
setGuesses(newGuesses);
setCurrentGuess([]);
if (currentGuess.join('').toUpperCase() === wordOfTheDay.toUpperCase()) {
setGameStatus('won');
}
if (newGuesses.length === 6 && currentGuess.join('').toUpperCase() !== wordOfTheDay.toUpperCase()) {
setGameStatus('lost');
}
};
return (
<
div
className="container"
>
<Head>
<title>Wordle Clone</title>
<meta name="description" content="A Wordle clone built with Next.js" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="main"
>
<h1 className="title">Wordle Clone</h1>
{confettiActive && (
<Confetti width={window.innerWidth} height={window.innerHeight} />
)}
<GameBoard
guesses={guesses}
currentGuess={currentGuess}
wordOfTheDay={wordOfTheDay}
gameStatus={gameStatus}
handleEnter={handleEnter}
/>
<Keyboard
onKeyPress={handleKeyPress}
guesses={guesses}
wordOfTheDay={wordOfTheDay}
gameStatus={gameStatus}
handleEnter={handleEnter}
handleDelete={handleDelete}
/>
{gameStatus !== 'playing' && (
<div className="game-over-message">
{gameStatus === 'won'
? 'Congratulations! You guessed the word!'
: `You lost! The word was ${wordOfTheDay.toUpperCase()}`}
</div>
)}
</main>
</div>
);
}
export default App;
Let’s break down this code:
- State Variables:
wordOfTheDay: Stores the secret word.guesses: An array of the player’s previous guesses.currentGuess: The player’s current guess.gameStatus: Tracks the game’s state (playing,won,lost).confettiActive: A boolean to control confetti display.- Fetching the Word of the Day: The
useEffecthook fetches the word of the day from a serverless function (which we will create next). - Key Press Handling:
handleKeyPressadds letters to thecurrentGuess. - Delete Handling:
handleDeleteremoves the last letter from thecurrentGuess. - Enter Handling:
handleEntersubmits thecurrentGuess, updates theguessesarray, and checks if the guess is correct or if the player has lost. - Game Over Message: Displays a message indicating whether the player won or lost.
Let’s add some basic styling to styles/globals.css:
html, body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
background-color: #f0f0f0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
min-height: 100vh;
padding: 0 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
text-align: center;
}
.game-over-message {
margin-top: 20px;
font-size: 1.5rem;
font-weight: bold;
text-align: center;
}
Creating the API Route for the Word of the Day
To get a new word each day, we’ll create a simple API route using Next.js API routes. Create a file named pages/api/word.js.
// pages/api/word.js
const wordList = [
'REACT',
'QUERY',
'BUILD',
'FRAME',
'WORLD',
'HELLO',
'APPLE',
'PIXEL',
'CLOUD',
'DEBUG',
'CACHE',
'ARRAY',
'INPUT',
'ERROR',
'INDEX',
'MODEL',
'STYLE',
'TABLE',
'VALUE',
'WHALE'
];
export default function handler(req, res) {
const today = new Date();
const wordIndex = today.getDate() % wordList.length;
const word = wordList[wordIndex];
res.status(200).json({ word });
}
Let’s break down this code:
- Word List: We have a list of words. In a real-world application, you’d likely fetch this from a database or a more extensive word list.
- API Route Handler: The
handlerfunction is the core of the API route. - Word Selection: We use the current date to determine the word of the day. This ensures a new word each day.
- Response: We return the word in a JSON response.
Running the Application
Now, let’s run our application. In your terminal, make sure you’re in the project directory and run:
npm run dev
This will start the development server. Open your browser and go to http://localhost:3000. You should see your Wordle clone!
Common Mistakes and Troubleshooting
Here are some common mistakes and how to fix them:
- Incorrect File Paths: Double-check that all file paths (e.g., in your import statements) are correct.
- Typos: Typos in your code can cause errors. Carefully review your code for any mistakes.
- State Updates: Make sure you’re updating state correctly using the
set...functions provided by theuseStatehook. - CSS Issues: If your styling isn’t working, check your CSS class names and make sure your CSS files are properly linked.
- API Errors: If you’re having trouble fetching the word of the day, check the browser’s developer console for any network errors. Also, verify that your API route is working correctly by visiting
http://localhost:3000/api/wordin your browser. - CORS Errors: If you are making API calls to a different domain, you might encounter CORS (Cross-Origin Resource Sharing) errors. This is a browser security feature. In a production environment, you’ll need to configure CORS on your server. For local development, you might be able to use a browser extension to disable CORS, but this is not recommended for production.
Key Takeaways and Next Steps
Congratulations! You’ve successfully built a Wordle clone using Next.js. Here’s a summary of what you’ve learned:
- Next.js Project Setup: You’ve set up a Next.js project and understood its basic file structure.
- Component Creation: You’ve created reusable React components (
GameBoard,Keyboard) to build the UI. - State Management: You’ve used the
useStatehook to manage the game’s state. - Event Handling: You’ve handled user input and interactions.
- API Integration: You’ve created a simple API route to fetch the word of the day.
- Styling with CSS: You’ve styled your components using CSS.
Here are some ideas for taking your Wordle clone to the next level:
- Implement a Dictionary API: Use a dictionary API to validate the player’s guesses.
- Add Difficulty Levels: Allow the player to choose the word length or the number of guesses.
- Improve the UI/UX: Add animations, sound effects, and more visual feedback.
- Save Game State: Use local storage to save the player’s progress.
- Implement User Accounts: Allow users to track their statistics and compete with others.
- Add a Settings Menu: Allow users to customize the game’s appearance and behavior.
- Deploy Your App: Deploy your Wordle clone to a platform like Vercel or Netlify to share it with the world.
FAQ
Here are some frequently asked questions about building a Wordle clone:
- How can I deploy my Wordle clone? You can deploy your Next.js application to platforms like Vercel, Netlify, or AWS. These platforms provide easy deployment workflows and handle the server-side rendering and build process for you.
- How can I add more words to the game? You can expand the
wordListarray in your API route or connect to a database to store a larger word list. Consider using a word list API for a more extensive selection. - How can I make the game responsive? Use responsive CSS techniques (e.g., media queries, flexbox) to ensure that your game looks good on different screen sizes.
- How can I prevent cheating? While it’s impossible to completely prevent cheating, you can make it more difficult by obfuscating your client-side code, validating guesses on the server-side, and implementing rate limiting.
This tutorial provides a solid foundation for building your own Wordle clone. Experiment with different features, and don’t be afraid to explore new technologies and techniques. The world of web development is constantly evolving, so keep learning and building. Remember, the best way to learn is by doing. Happy coding!
This project, while seemingly simple, brings together a variety of essential web development concepts. From understanding component-based architecture to managing state and handling user interactions, building a Wordle clone provides a practical and engaging way to solidify your skills. As you continue to refine and expand upon this project, you’ll not only enhance your technical abilities but also gain a deeper appreciation for the intricacies of web application development. The journey of building is, in itself, the most rewarding aspect. Keep exploring, keep building, and keep learning.
