Build a Wordle Clone with JavaScript
Build a playable Wordle clone using DOM manipulation and event handling.
Introduction
30 Second Summary
Wordle became a daily ritual for millions of people because of one simple loop: guess a word, see which letters light up, and try again. Building that loop yourself teaches you exactly how interactive web apps work under the hood.
In this project, you will build a fully playable Wordle clone from scratch using vanilla JavaScript, HTML, and CSS. You'll handle keyboard input, check guesses against a target word, and animate color reveals tile by tile.
What You'll Build
A browser-based word-guessing game where the player has 6 attempts to guess a hidden 5-letter word, with animated tile-flip color reveals after each guess.
By the end of this project, you'll have:
- A complete Wordle game with keyboard input handling, animated tile reveals, and win/lose detection.
- Hands-on experience with core JavaScript concepts: DOM manipulation, event listeners, and game state management.
- A working understanding of how to build interactive browser applications without any frameworks or build tools.
- Secret Mission: Add an on-screen virtual keyboard that visually tracks which letters have been used and their status.
Are there any prerequisites?
You should have basic HTML and CSS knowledge. No prior JavaScript experience is needed. This project teaches you JavaScript fundamentals from the ground up.
Before We Start
Before you can build your Wordle clone, you need a development environment that lets you see changes in real time.
In this step, you'll install a VS Code extension called Live Server that auto-refreshes your browser whenever you save a file. You'll also create the project folder and files your game needs.
In this step, get ready to:
- Install the Live Server extension in VS Code.
- Create the project folder and files.
- Verify Live Server works by opening a test page in your browser.
Install the Live Server extension
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Press Cmd+Shift+X to open the Extensions panel.
- Type Live Server in the search bar.
- Find the one by Ritwick Dey and click Install.
What is Live Server?
Live Server runs a small local web server on your machine and automatically reloads the page every time you save a file. This gives you an instant feedback loop while building your game.
Create the project folder and files
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Create your project folder and files by running these commands:
mkdir ~/wordle-clone
cd ~/wordle-clone
touch index.html style.css script.js
- Confirm the files exist by running:
ls
You should see index.html, style.css, and script.js in the output.
- Open the folder in VS Code by running:
code .
A new VS Code window opens with your wordle-clone folder loaded in the sidebar.
VS Code didn't open?
On macOS, the code command needs to be added to your PATH manually. Open VS Code, press Cmd+Shift+P to open the Command Palette, type shell command, and select Shell Command: Install 'code' command in PATH.
Restart your Terminal and try code . again. If it still doesn't work, open VS Code manually and use File > Open Folder to open the wordle-clone folder from your home directory.
help me fix the code command in my terminal
Verify Live Server works
Now you'll add a basic HTML page and open it with Live Server to confirm everything is connected.
- In VS Code, click on index.html in the sidebar to open the file.
- Add the following HTML boilerplate to index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Wordle Clone</title>
</head>
<body>
</body>
</html>
What does this code do?
This is a minimal HTML document structure. <!DOCTYPE html> tells the browser to use modern HTML5 standards. The <meta charset="UTF-8"> tag ensures special characters display correctly.
- Save the file with Cmd+S.
- Right-click anywhere in the index.html file in the Explorer sidebar.
- Select Open with Live Server.
Your default browser opens with a blank page at a URL like http://127.0.0.1:5500. The blank page is expected because the <body> tag is empty.
Browser didn't open?
Make sure you right-clicked the file in the Explorer sidebar on the left (not in the editor area). You can also click Go Live in the bottom-right status bar of VS Code.
If you don't see the Open with Live Server option, confirm the extension installed successfully by pressing Cmd+Shift+X and checking that Live Server appears under Installed.
help me get Live Server working
✔️ Awesome, I've got everything!
Great. Double check you've saved index.html and that your browser is showing the blank page.
ⓧ I'd like to double check the full code
Here is what index.html should look like at this point:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Wordle Clone</title>
</head>
<body>
</body>
</html>
The style.css and script.js files should be empty.
Your development environment is ready. Next up, you'll build the game board that holds the letter tiles.
Build the Game Board
Every game needs a playing field. For Wordle, that means a 6-row by 5-column grid of tiles where letters appear as the player types guesses.
In this step, you will build the game board's HTML structure, style it with CSS Grid, and add the tile-flip animation that makes Wordle feel satisfying to play.
In this step, get ready to:
- Create the HTML structure for the game board, title, and message area.
- Style the board with CSS Grid and define color classes for tile states.
- Add the tile-flip animation using a CSS transform and transition.
Create the HTML structure
The game needs four main elements: a title, a message area for win/lose feedback, the tile grid, and a play-again button.
- Switch to index.html in VS Code and replace all the existing content with this HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wordle Clone</title>
<!-- Link the stylesheet -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header>
<h1>Wordle</h1>
</header>
<!-- Message area for win/lose feedback -->
<div id="message"></div>
<!-- The game board grid (tiles created by JavaScript later) -->
<div id="board"></div>
<!-- Replay button, hidden until game ends -->
<button id="play-again" class="hidden">Play Again</button>
</div>
<!-- Link the JavaScript file -->
<script src="script.js"></script>
</body>
</html>
What does this HTML do?
- The <div id="board"> is an empty container. JavaScript will fill it with tile elements in the next step.
- The <div id="message"> is where win/lose text will appear after a game ends.
- The <button id="play-again"> starts hidden (via the hidden class) and only shows after the game finishes.
- The <link> and <script> tags connect your CSS and JavaScript files to the HTML.
- Save index.html.
- Check your browser. You should see just the word "Wordle" on a white page. The page will look much better once we add CSS next.
Don't see anything in the browser?
- Make sure Live Server is still running. Look for the "Port: 5500" indicator in the VS Code status bar at the bottom. If it disappeared, right-click index.html in the Explorer sidebar and select Open with Live Server.
- Confirm you saved the file. Unsaved changes show a dot on the file tab in VS Code.
Help me troubleshoot why my HTML page is blank in the browser.
✔️ Awesome, I've got everything!
Great. Make sure you have saved index.html before moving on to the CSS.
ⓧ I'd like to double check the full code
Here is the complete index.html file at this point:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wordle Clone</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<header>
<h1>Wordle</h1>
</header>
<div id="message"></div>
<div id="board"></div>
<button id="play-again" class="hidden">Play Again</button>
</div>
<script src="script.js"></script>
</body>
</html>
Style the game board with CSS
CSS Grid is perfect for Wordle's tile layout. Each tile is a fixed-size square, and the grid arranges them into 5 columns and 6 rows automatically.
- Switch to style.css in VS Code and paste the following base layout styles:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #121213;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
header h1 {
font-size: 2rem;
letter-spacing: 0.2rem;
text-transform: uppercase;
}
#message {
font-size: 1.2rem;
min-height: 1.5rem;
font-weight: bold;
}
What do these styles do?
- The * selector resets all margins and padding so every browser starts from the same baseline.
- The body uses Flexbox to center all content vertically and horizontally. The dark #121213 background matches Wordle's signature dark theme.
- The .container stacks elements in a column with consistent spacing using gap: 20px.
- The #message div reserves space (min-height: 1.5rem) so the layout does not shift when text appears later.
- Save style.css and check your browser.
- You should see "WORDLE" in white text centered on a dark background.
Still seeing a white background?
- Make sure the <link rel="stylesheet" href="style.css"> line exists in your index.html <head> section. Without it, the browser cannot find your styles.
- Confirm the filename is exactly style.css (not styles.css or Style.css). The filename must match the href value.
Help me figure out why my CSS styles are not loading.
- Now add the game board grid and tile styles below the existing CSS in style.css:
#board {
display: grid;
grid-template-columns: repeat(5, 62px);
grid-template-rows: repeat(6, 62px);
gap: 5px;
}
.tile {
width: 62px;
height: 62px;
border: 2px solid #3a3a3c;
display: flex;
justify-content: center;
align-items: center;
font-size: 2rem;
font-weight: bold;
text-transform: uppercase;
transition: transform 0.5s ease;
}
.tile.filled {
border-color: #565758;
}
.tile.correct {
background-color: #538d4e;
border-color: #538d4e;
}
.tile.present {
background-color: #b59f3b;
border-color: #b59f3b;
}
.tile.absent {
background-color: #3a3a3c;
border-color: #3a3a3c;
}
What do the grid and tile styles do?
- CSS Grid on #board creates a 5-column, 6-row layout. Each cell is 62px with a 5px gap between tiles.
- The .tile class defines the base look: a square with a subtle border, centered text, and a transition property that enables smooth animation later.
- The color classes (.correct, .present, .absent) use Wordle's signature green, yellow, and gray. JavaScript will add these classes to tiles after checking each guess.
- Now add the button and utility styles at the bottom of style.css:
#play-again {
padding: 10px 24px;
font-size: 1rem;
font-weight: bold;
background-color: #538d4e;
color: #ffffff;
border: none;
border-radius: 4px;
cursor: pointer;
}
#play-again:hover {
background-color: #6aae5e;
}
.hidden {
display: none;
}
What do these styles do?
- The #play-again button uses the same green as correct tiles, keeping the design consistent.
- The .hidden class uses display: none to completely remove an element from the page. The play-again button starts hidden and only appears when the game ends.
- Save style.css.
- Your browser still shows just the "WORDLE" title on the dark background. The grid is not visible yet because the #board div has no child elements. JavaScript will create the tiles in the next step.
Seeing a CSS syntax error or broken layout?
- Check that every opening brace { has a matching closing brace }. A missing brace will break all styles below it.
- Make sure every property line ends with a semicolon. A missing semicolon causes the next property to be ignored.
Help me find the CSS syntax error in my style.css file.
Add the tile-flip animation
The signature Wordle reveal uses a tile flip. This works by combining a CSS transition (already set on .tile) with a class that triggers a rotation transform.
- In style.css, find the .tile.filled selector. Add this new class directly below it (above .tile.correct):
.tile.reveal {
transform: rotateX(360deg);
}
How does the flip animation work?
Two CSS properties work together to create the flip effect.
The transition: transform 0.5s ease on .tile tells the browser to animate any transform change over half a second. The .reveal class adds rotateX(360deg), which spins the tile around its horizontal axis. When JavaScript adds the reveal class to a tile, the transition kicks in and you see a smooth flip.
- Save style.css. You will not see the animation yet. JavaScript will trigger it by adding the reveal class to tiles at the right moment in a later step.
✔️ Awesome, I've got everything!
Great. Double check you have saved style.css before moving on.
ⓧ I'd like to double check the full code
Here is the complete style.css file at this point:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #121213;
color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
header h1 {
font-size: 2rem;
letter-spacing: 0.2rem;
text-transform: uppercase;
}
#message {
font-size: 1.2rem;
min-height: 1.5rem;
font-weight: bold;
}
#board {
display: grid;
grid-template-columns: repeat(5, 62px);
grid-template-rows: repeat(6, 62px);
gap: 5px;
}
.tile {
width: 62px;
height: 62px;
border: 2px solid #3a3a3c;
display: flex;
justify-content: center;
align-items: center;
font-size: 2rem;
font-weight: bold;
text-transform: uppercase;
transition: transform 0.5s ease;
}
.tile.filled {
border-color: #565758;
}
.tile.reveal {
transform: rotateX(360deg);
}
.tile.correct {
background-color: #538d4e;
border-color: #538d4e;
}
.tile.present {
background-color: #b59f3b;
border-color: #b59f3b;
}
.tile.absent {
background-color: #3a3a3c;
border-color: #3a3a3c;
}
#play-again {
padding: 10px 24px;
font-size: 1rem;
font-weight: bold;
background-color: #538d4e;
color: #ffffff;
border: none;
border-radius: 4px;
cursor: pointer;
}
#play-again:hover {
background-color: #6aae5e;
}
.hidden {
display: none;
}
Your game board's visual foundation is ready. Next up, you will add JavaScript to capture keyboard input and fill the tiles with letters as the player types.
Handle Keyboard Input
Your game board is styled and visible in the browser. Now it's time to make it interactive.
Every game needs a way to receive player input. In Wordle, the player types letters, uses Backspace to fix mistakes, and presses Enter to submit a guess. JavaScript can listen for these key presses and update the board in real time.
In this step, get ready to:
- Set up game state variables and dynamically create the tile grid.
- Listen for keyboard input and display letters in the correct tiles.
Set up game state and build the board
Before the game can respond to keyboard input, it needs a few things in place: constants defining the board size, variables tracking the player's position, and a function that creates the tile elements on screen.
- Switch to script.js in VS Code and add the game setup code by pasting the following:
// Game constants
const WORD_LENGTH = 5;
const MAX_GUESSES = 6;
// Track where the player is on the board
let currentRow = 0;
let currentTile = 0;
let gameOver = false;
// Grab references to HTML elements
const board = document.getElementById("board");
const message = document.getElementById("message");
const playAgainBtn = document.getElementById("play-again");
// Build the 6x5 grid of tiles dynamically
function createBoard() {
for (let i = 0; i < MAX_GUESSES * WORD_LENGTH; i++) {
const tile = document.createElement("div");
tile.classList.add("tile");
tile.id = "tile-" + i;
board.appendChild(tile);
}
}
// Get the 5 tiles in the current row
function getCurrentRowTiles() {
const startIndex = currentRow * WORD_LENGTH;
const tiles = [];
for (let i = startIndex; i < startIndex + WORD_LENGTH; i++) {
tiles.push(document.getElementById("tile-" + i));
}
return tiles;
}
createBoard();
What does this code do?
- WORD_LENGTH and MAX_GUESSES are constants that define the board dimensions. Using named constants makes the code easier to read than bare numbers.
- currentRow and currentTile track where the next letter should go. Together they act as a cursor on the board.
- createBoard() generates 30 div elements (6 rows x 5 columns) and adds them to the #board container. Each tile gets a unique ID like tile-0 through tile-29 so we can target it later.
- getCurrentRowTiles() returns an array of the 5 tile elements in whatever row the player is currently on. This helper keeps the keyboard handler clean.
- Save script.js and check your browser.
- You should see the same 6x5 grid of bordered empty tiles. The tiles are now created by JavaScript instead of being hardcoded in HTML.
Grid disappeared or looks wrong?
- Open the browser console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and look for red error messages. A typo in getElementById or a missing quote will show up there.
- Make sure the element IDs in your JavaScript ("board", "message", "play-again") match the IDs in your index.html exactly.
- help me debug why my board tiles aren't showing up
Add a keydown event listener
The browser fires a keydown event every time a key is pressed. By attaching a listener to the document, your game can capture every keystroke and decide what to do with it.
- In script.js, find the createBoard(); line at the bottom of your file.
- Add the following code directly above that line:
function handleKeyPress(event) {
// Stop processing if game is over
if (gameOver) return;
const key = event.key;
// Submit the guess when Enter is pressed
if (key === "Enter") {
if (currentTile === WORD_LENGTH) {
checkGuess();
}
return;
}
// Remove the last letter when Backspace is pressed
if (key === "Backspace") {
if (currentTile > 0) {
currentTile--;
const tiles = getCurrentRowTiles();
tiles[currentTile].textContent = "";
tiles[currentTile].classList.remove("filled");
}
return;
}
// Place a letter in the current tile
if (key.length === 1 && key >= "a" && key <= "z") {
if (currentTile < WORD_LENGTH) {
const tiles = getCurrentRowTiles();
tiles[currentTile].textContent = key;
tiles[currentTile].classList.add("filled");
currentTile++;
}
}
}
document.addEventListener("keydown", handleKeyPress);
What does this code do?
The handleKeyPress function runs every time any key is pressed. It reads event.key to figure out which key was pressed, then handles three cases:
- Letter keys (a-z): the check key.length === 1 && key >= "a" && key <= "z" filters out modifier keys and non-letter keys. If the row isn't full, the letter is placed in the current tile and the cursor advances.
- Backspace: moves the cursor back one tile, clears its text, and removes the filled class (which lightens the border).
- Enter: only fires checkGuess() when all 5 tiles are filled. This prevents partial submissions. The checkGuess function doesn't exist yet, so pressing Enter on a full row will show a console error for now.
- Save script.js and switch to your browser.
- Click anywhere on the page to make sure it has focus, then type a few letters. You should see them appear in the first row of tiles.
- Press Backspace to delete the last letter. It should disappear.
- Type 5 letters and press Enter. You'll see a console error mentioning checkGuess is not defined. That's expected and confirms Enter is wired up correctly.
Letters not appearing when you type?
- Click directly on the page first. If the browser's address bar or DevTools panel has focus, keystrokes won't reach your document listener.
- Check that document.addEventListener("keydown", handleKeyPress); appears between the handleKeyPress function and the createBoard(); call at the bottom.
- If you see uppercase letters in the tiles, make sure Caps Lock is off. The code only accepts lowercase a through z.
- help me figure out why my keydown listener isn't working
✔️ Awesome, I've got everything!
Great. Double check you've saved script.js and that typing letters fills the grid.
ⓧ I'd like to double check the full code
Here's the complete script.js at this point:
const WORD_LENGTH = 5;
const MAX_GUESSES = 6;
let currentRow = 0;
let currentTile = 0;
let gameOver = false;
const board = document.getElementById("board");
const message = document.getElementById("message");
const playAgainBtn = document.getElementById("play-again");
function createBoard() {
for (let i = 0; i < MAX_GUESSES * WORD_LENGTH; i++) {
const tile = document.createElement("div");
tile.classList.add("tile");
tile.id = "tile-" + i;
board.appendChild(tile);
}
}
function getCurrentRowTiles() {
const startIndex = currentRow * WORD_LENGTH;
const tiles = [];
for (let i = startIndex; i < startIndex + WORD_LENGTH; i++) {
tiles.push(document.getElementById("tile-" + i));
}
return tiles;
}
function handleKeyPress(event) {
if (gameOver) return;
const key = event.key;
if (key === "Enter") {
if (currentTile === WORD_LENGTH) {
checkGuess();
}
return;
}
if (key === "Backspace") {
if (currentTile > 0) {
currentTile--;
const tiles = getCurrentRowTiles();
tiles[currentTile].textContent = "";
tiles[currentTile].classList.remove("filled");
}
return;
}
if (key.length === 1 && key >= "a" && key <= "z") {
if (currentTile < WORD_LENGTH) {
const tiles = getCurrentRowTiles();
tiles[currentTile].textContent = key;
tiles[currentTile].classList.add("filled");
currentTile++;
}
}
}
document.addEventListener("keydown", handleKeyPress);
createBoard();
Your game now responds to every keystroke. Next up, you'll add the word list and build the logic that checks each guess against the target word, revealing green, yellow, and gray tiles.
Check the Guess and Reveal Colors
Your grid accepts letters and responds to Backspace. The board looks and feels interactive, but nothing happens when you press Enter.
In this step, you will add the brain of the game. The game needs to know the secret word, compare it to the player's guess, and then reveal the results with an animated color flip.
In this step, get ready to:
- Create the word list and randomly select a target word.
- Write the guess-checking algorithm that handles duplicate letters.
- Animate the tile-flip color reveal with staggered timing.
Create the word list and target word
Every Wordle game starts by secretly picking one word from a list. The player never sees which word was chosen until the game is over.
- In script.js, add the following code at the very top of the file, above the existing WORD_LENGTH line:
const WORDS = [
"crane", "slate", "trace", "crate", "stone",
"share", "shine", "those", "thing", "think",
"plant", "place", "plane", "plate", "plain",
"brain", "bread", "break", "brave", "brand",
"light", "sight", "night", "right", "might",
"house", "mouse", "found", "sound", "round"
];
What does this code do?
This array holds 30 common five-letter English words. The game will randomly pick one of these as the secret answer each time the page loads.
- Now find the line that says let currentRow = 0; and add this new variable directly above it:
// Pick a random word from the list as the secret target
let targetWord = WORDS[Math.floor(Math.random() * WORDS.length)];
How does the random selection work?
Math.random() generates a decimal between 0 and 1. Multiplying by the array length and rounding down with Math.floor() gives a random whole-number index. The result is a different secret word every time the page refreshes.
- Save script.js.
- Open the browser console (press Cmd+Option+J on macOS or Ctrl+Shift+J on Windows). Type targetWord and press Enter. You should see one of the words from your list printed in the console.
Seeing 'undefined' in the console?
Make sure the WORDS array is at the very top of script.js, above WORD_LENGTH. Also confirm that targetWord is placed after MAX_GUESSES and before currentRow.
Help me debug why targetWord is undefined in my Wordle game
Write the checkGuess function
The core challenge of Wordle is comparing the player's guess to the target word. A letter can be correct (right letter, right position), present (right letter, wrong position), or absent (not in the word at all).
Duplicate letters make this tricky. If the target word has one "e" but the guess has two, only one should light up. The solution is a two-pass approach: check exact matches first, then check remaining letters.
- In script.js, add the checkGuess function directly below the closing } of the handleKeyPress function:
function checkGuess() {
// Get the current row's tiles and build the guess string
const tiles = getCurrentRowTiles();
const guess = tiles.map(tile => tile.textContent).join("");
// Create arrays to track which letters have been matched
const result = [];
const targetLetters = targetWord.split("");
const guessLetters = guess.split("");
// First pass: mark exact matches (correct position)
for (let i = 0; i < WORD_LENGTH; i++) {
if (guessLetters[i] === targetLetters[i]) {
result[i] = "correct";
targetLetters[i] = null;
guessLetters[i] = null;
}
}
// Second pass: mark present or absent letters
for (let i = 0; i < WORD_LENGTH; i++) {
if (guessLetters[i] === null) continue;
const index = targetLetters.indexOf(guessLetters[i]);
if (index !== -1) {
result[i] = "present";
targetLetters[index] = null;
} else {
result[i] = "absent";
}
}
revealRow(tiles, result);
}
Why two passes instead of one?
Imagine the target word is "crane" and the guess is "creep". Both have the letter "e", but only one position is correct. The first pass locks in exact matches and sets those letters to null so they cannot be double-counted.
The second pass then scans the remaining unmatched letters. If a letter exists somewhere in the target (but not in the same position), it gets marked "present". Otherwise it is "absent". This two-pass algorithm is the same approach the real Wordle uses.
- Save script.js. You will not see a visible change yet because checkGuess calls revealRow, which you have not written yet.
Add the animated tile reveal
The signature Wordle moment is watching tiles flip one by one to reveal their color. You will use setTimeout to stagger each tile's animation by 300 milliseconds, creating the sequential reveal effect.
- In script.js, add the revealRow function directly below the closing } of the checkGuess function:
function revealRow(tiles, result) {
// Flip each tile with a staggered delay
tiles.forEach((tile, index) => {
setTimeout(() => {
tile.classList.add("reveal", result[index]);
}, index * 300);
});
// After all tiles have flipped, advance to the next row
setTimeout(() => {
currentRow++;
currentTile = 0;
}, WORD_LENGTH * 300);
}
What does this code do?
The forEach loop schedules each tile to flip at increasing intervals. The first tile flips at 0ms, the second at 300ms, the third at 600ms, and so on. Adding the reveal class triggers the CSS rotateX(360deg) transform you styled earlier.
The final setTimeout waits for all five tiles to finish flipping (5 x 300ms = 1500ms), then advances to the next row and resets the tile index so the player can type their next guess.
- Save script.js and switch to your browser.
- Type any 5-letter word (like crane) and press Enter. You should see the tiles flip one by one, each revealing a green, yellow, or gray background color.
Tiles not flipping or showing color?
Check that the revealRow function is placed after checkGuess and before the document.addEventListener line at the bottom of your file.
Also confirm your CSS file still has the .tile.reveal, .tile.correct, .tile.present, and .tile.absent classes. If the console shows an error like "revealRow is not defined", the function name may be misspelled.
Help me debug why my Wordle tiles aren't flipping or showing colors after I press Enter
✔️ Awesome, I've got everything!
Great. Double check you have saved script.js before moving on.
ⓧ I'd like to double check the full code
const WORDS = [
"crane", "slate", "trace", "crate", "stone",
"share", "shine", "those", "thing", "think",
"plant", "place", "plane", "plate", "plain",
"brain", "bread", "break", "brave", "brand",
"light", "sight", "night", "right", "might",
"house", "mouse", "found", "sound", "round"
];
const WORD_LENGTH = 5;
const MAX_GUESSES = 6;
let targetWord = WORDS[Math.floor(Math.random() * WORDS.length)];
let currentRow = 0;
let currentTile = 0;
let gameOver = false;
const board = document.getElementById("board");
const message = document.getElementById("message");
const playAgainBtn = document.getElementById("play-again");
function createBoard() {
for (let i = 0; i < MAX_GUESSES * WORD_LENGTH; i++) {
const tile = document.createElement("div");
tile.classList.add("tile");
tile.id = "tile-" + i;
board.appendChild(tile);
}
}
function getCurrentRowTiles() {
const startIndex = currentRow * WORD_LENGTH;
const tiles = [];
for (let i = startIndex; i < startIndex + WORD_LENGTH; i++) {
tiles.push(document.getElementById("tile-" + i));
}
return tiles;
}
function handleKeyPress(event) {
if (gameOver) return;
const key = event.key;
if (key === "Enter") {
if (currentTile === WORD_LENGTH) {
checkGuess();
}
return;
}
if (key === "Backspace") {
if (currentTile > 0) {
currentTile--;
const tiles = getCurrentRowTiles();
tiles[currentTile].textContent = "";
tiles[currentTile].classList.remove("filled");
}
return;
}
if (key.length === 1 && key >= "a" && key <= "z") {
if (currentTile < WORD_LENGTH) {
const tiles = getCurrentRowTiles();
tiles[currentTile].textContent = key;
tiles[currentTile].classList.add("filled");
currentTile++;
}
}
}
function checkGuess() {
const tiles = getCurrentRowTiles();
const guess = tiles.map(tile => tile.textContent).join("");
const result = [];
const targetLetters = targetWord.split("");
const guessLetters = guess.split("");
for (let i = 0; i < WORD_LENGTH; i++) {
if (guessLetters[i] === targetLetters[i]) {
result[i] = "correct";
targetLetters[i] = null;
guessLetters[i] = null;
}
}
for (let i = 0; i < WORD_LENGTH; i++) {
if (guessLetters[i] === null) continue;
const index = targetLetters.indexOf(guessLetters[i]);
if (index !== -1) {
result[i] = "present";
targetLetters[index] = null;
} else {
result[i] = "absent";
}
}
revealRow(tiles, result);
}
function revealRow(tiles, result) {
tiles.forEach((tile, index) => {
setTimeout(() => {
tile.classList.add("reveal", result[index]);
}, index * 300);
});
setTimeout(() => {
currentRow++;
currentTile = 0;
}, WORD_LENGTH * 300);
}
document.addEventListener("keydown", handleKeyPress);
createBoard();
Your tiles now flip and light up with feedback colors. Next up, you will add win and lose detection so the game knows when it is over.
Win/Lose Detection and Game Over
Your game board lights up with green, yellow, and gray tiles after each guess. But right now, nothing actually happens when you guess correctly or run out of attempts.
In this step, you will add the logic that detects a win or a loss, displays the appropriate message, and gives the player a way to start over.
In this step, get ready to:
- Add win/lose detection after each guess is revealed.
- Display a status message for winning or losing.
- Wire up the Play Again button to restart the game.
Detect a win or loss after each guess
After all five tiles in a row finish flipping, the game needs to check whether the guess matches the target word. The revealRow function already has a setTimeout that fires after all tiles flip. That is the perfect place to add the check.
First, you need to pass the player's guess into revealRow so it can compare the guess to the target word.
- In script.js, scroll to the checkGuess function and find this line at the very end of it:
revealRow(tiles, result);
- Replace it with this line, which passes the guess string as a third argument:
revealRow(tiles, result, guess);
Now revealRow will know what the player typed so it can compare it to the target word.
- Next, find the entire revealRow function and replace it with this updated version:
function revealRow(tiles, result, guess) {
tiles.forEach((tile, index) => {
setTimeout(() => {
tile.classList.add("reveal", result[index]);
}, index * 300);
});
setTimeout(() => {
// Check for win: the guess matches the target word
if (guess === targetWord) {
message.textContent = "You won!";
gameOver = true;
playAgainBtn.classList.remove("hidden");
// Check for loss: all 6 attempts used up
} else if (currentRow === MAX_GUESSES - 1) {
message.textContent = "The word was: " + targetWord.toUpperCase();
gameOver = true;
playAgainBtn.classList.remove("hidden");
}
currentRow++;
currentTile = 0;
}, WORD_LENGTH * 300);
}
What does this code do?
The function signature now accepts a third parameter, guess, which is the five-letter string the player typed.
Inside the second setTimeout (which fires after all tiles finish flipping), the code compares guess to targetWord. If they match, the player wins. If the current row is the last row (index 5, which is MAX_GUESSES - 1), the player has lost.
In both cases, the code sets gameOver = true and removes the hidden class from the Play Again button to reveal it.
How does the game stop accepting input?
The handleKeyPress function already has if (gameOver) return; at the top. Once revealRow sets gameOver to true, every future key press is ignored automatically.
Add the Play Again button listener
When the game ends, the green Play Again button appears. It needs a click handler that resets everything by reloading the page.
- In script.js, scroll to the bottom of the file and find this line:
document.addEventListener("keydown", handleKeyPress);
- Add the following code directly above that line:
// Reload the page to start a fresh game
playAgainBtn.addEventListener("click", () => {
window.location.reload();
});
What does this code do?
When the player clicks the Play Again button, window.location.reload() refreshes the page. This re-runs script.js from the top, which picks a new random word from the WORDS array, resets all state variables to zero, and rebuilds the empty board.
- Save script.js.
- Switch to your browser. To test the win path, open the browser console with Cmd+Option+J and type targetWord to peek at the answer.
- Type that word into the game and press Enter.
- You should see "You won!" appear above the board and a green Play Again button below it.
- Click Play Again to confirm the page reloads and the board resets.
Not seeing the win message or button?
Make sure you updated the revealRow function signature to accept three parameters: tiles, result, and guess.
Also check that checkGuess now passes guess as the third argument in the call to revealRow(tiles, result, guess). If you still pass only two arguments, guess inside revealRow will be undefined and the comparison will always fail.
Help me debug why my Wordle win/lose detection isn't working.
Test the losing path
You confirmed winning works. Now verify the loss path behaves correctly too.
- Click Play Again to start a fresh game.
- Type six incorrect 5-letter guesses (e.g. type aaaaa and press Enter six times).
- After the sixth guess flips, you should see "The word was: " followed by the target word in uppercase.
- Try pressing letter keys after the game ends. Nothing should happen because the gameOver flag blocks all input.
Game doesn't stop after six guesses?
Check that your loss condition uses currentRow === MAX_GUESSES - 1 (which equals 5). The check runs before currentRow++, so the sixth guess is at index 5.
Help me figure out why my Wordle game doesn't end after six guesses.
✔️ Awesome, I've got everything!
Great. Double check you have saved script.js and your game is fully playable.
ⓧ I'd like to double check the full code
const WORDS = [
"crane", "slate", "trace", "crate", "stone",
"share", "shine", "those", "thing", "think",
"plant", "place", "plane", "plate", "plain",
"brain", "bread", "break", "brave", "brand",
"light", "sight", "night", "right", "might",
"house", "mouse", "found", "sound", "round"
];
const WORD_LENGTH = 5;
const MAX_GUESSES = 6;
let targetWord = WORDS[Math.floor(Math.random() * WORDS.length)];
let currentRow = 0;
let currentTile = 0;
let gameOver = false;
const board = document.getElementById("board");
const message = document.getElementById("message");
const playAgainBtn = document.getElementById("play-again");
function createBoard() {
for (let i = 0; i < MAX_GUESSES * WORD_LENGTH; i++) {
const tile = document.createElement("div");
tile.classList.add("tile");
tile.id = "tile-" + i;
board.appendChild(tile);
}
}
function getCurrentRowTiles() {
const startIndex = currentRow * WORD_LENGTH;
const tiles = [];
for (let i = startIndex; i < startIndex + WORD_LENGTH; i++) {
tiles.push(document.getElementById("tile-" + i));
}
return tiles;
}
function handleKeyPress(event) {
if (gameOver) return;
const key = event.key;
if (key === "Enter") {
if (currentTile === WORD_LENGTH) {
checkGuess();
}
return;
}
if (key === "Backspace") {
if (currentTile > 0) {
currentTile--;
const tiles = getCurrentRowTiles();
tiles[currentTile].textContent = "";
tiles[currentTile].classList.remove("filled");
}
return;
}
if (key.length === 1 && key >= "a" && key <= "z") {
if (currentTile < WORD_LENGTH) {
const tiles = getCurrentRowTiles();
tiles[currentTile].textContent = key;
tiles[currentTile].classList.add("filled");
currentTile++;
}
}
}
function checkGuess() {
const tiles = getCurrentRowTiles();
const guess = tiles.map(tile => tile.textContent).join("");
const result = [];
const targetLetters = targetWord.split("");
const guessLetters = guess.split("");
for (let i = 0; i < WORD_LENGTH; i++) {
if (guessLetters[i] === targetLetters[i]) {
result[i] = "correct";
targetLetters[i] = null;
guessLetters[i] = null;
}
}
for (let i = 0; i < WORD_LENGTH; i++) {
if (guessLetters[i] === null) continue;
const index = targetLetters.indexOf(guessLetters[i]);
if (index !== -1) {
result[i] = "present";
targetLetters[index] = null;
} else {
result[i] = "absent";
}
}
revealRow(tiles, result, guess);
}
function revealRow(tiles, result, guess) {
tiles.forEach((tile, index) => {
setTimeout(() => {
tile.classList.add("reveal", result[index]);
}, index * 300);
});
setTimeout(() => {
if (guess === targetWord) {
message.textContent = "You won!";
gameOver = true;
playAgainBtn.classList.remove("hidden");
} else if (currentRow === MAX_GUESSES - 1) {
message.textContent = "The word was: " + targetWord.toUpperCase();
gameOver = true;
playAgainBtn.classList.remove("hidden");
}
currentRow++;
currentTile = 0;
}, WORD_LENGTH * 300);
}
playAgainBtn.addEventListener("click", () => {
window.location.reload();
});
document.addEventListener("keydown", handleKeyPress);
createBoard();
Your Wordle clone is now fully playable. You can guess words, see the color reveals, win, lose, and start over. Next up, stretch your skills with the Secret Mission and add an on-screen virtual keyboard.
Secret mission
Add an On-Screen Virtual Keyboard
Your Wordle clone works great with a physical keyboard. But what about touch screens and mobile devices? In this secret mission, you'll build an interactive on-screen keyboard that visually tracks which letters have been guessed and their current status.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- The wordle-clone project folder containing index.html, style.css, and script.js.
- The Live Server VS Code extension.
Keep everything running
No action needed. Choose this if you want to keep playing your Wordle clone or continue tweaking the code.
- Your project files stay in ~/wordle-clone and cost nothing to keep on your machine.
- Live Server only runs while VS Code is open. There are no background processes.
Pause - I'll come back to this later
Stop the local server to free up the port, but keep your files for later.
- In VS Code, click Port: 5500 in the bottom status bar to stop Live Server.
- Close the VS Code window.
- Your files remain in ~/wordle-clone. Reopen the folder in VS Code and start Live Server again whenever you want to resume.
Delete - I don't want to use this again
Remove all project files and the extension you installed.
- Stop Live Server if it is still running, then close VS Code.
- Open Terminal and remove the project folder by running:
rm -rf ~/wordle-clone
- Confirm the folder is gone by running:
ls ~/wordle-clone
You should see a message like No such file or directory.
- To uninstall the Live Server extension, press Cmd+Shift+X in VS Code to open the Extensions panel.
- Find Live Server in your installed extensions list and click Uninstall.
That's a Wrap!
That's a Wrap!
Nice work! You just built a fully playable Wordle clone from scratch using only HTML, CSS, and vanilla JavaScript.
You've learned how to:
- Manipulate the DOM by selecting elements, adding and removing classes, and updating text content to build an interactive game board.
- Handle events by listening for keyboard input and responding to user actions with real-time UI updates.
- Manage game state by tracking guesses across rows, validating input length, handling duplicate letters with a two-pass algorithm, and controlling game flow with win/lose detection.
- Secret Mission: Added an interactive on-screen virtual keyboard with stateful color tracking that never downgrades a letter's status.
Ready to quiz yourself?