Building A Wordle Clone Using React

Building A Wordle Clone Using React

In the popular Wordle game, you must guess a five-letter word by typing on a keyboard. When you type the word, you get hints related to the word you're guessing. There're multiple hints you get while playing this game.

  • Green Letter - if it's correct and in the right position of an actual word.

  • Yellow Letter - if it's correct but not in the right position of an actual word.

  • Gray Letter - if it does not exist in the actual word.

Wordle Game

This is essentially what you'll learn to build in this guide: we'll construct a Wordle clone in React. All the animations will resemble the original Wordle game. You'll get a different word to guess with each page refresh. You need to have some basic understanding of React to build this game.

Setup

To set up the React project: Run npx create-react-app wordle command and then npm start to start the project. You'll have only three files (App.js, index.css, and index.js) inside the src folder. Delete all the other files to keep the src folder clean. Add the following code in the remaining files of the src folder.

//   src/App.js
function App() {
  return (
    <div className="App">
      <h1>WORDLE</h1>
    </div>
  );
}

export default App
/* src/index.css*/
body {
  text-align: center;
  font-size: 1em;
  font-family: verdana;
  margin: 0;
}
h1 {
  font-size: 1.2em;
  padding: 20px 0;
  border-bottom: 1px solid #eee;
  margin: 0 0 30px 0;
  color: #333;
}
//   src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Use JSON Server

You need a list of words to begin building of Wordle game. You'll randomly pick one of those words to start the game. We can fetch a word list from an external source. A JSON file is required to store our data. Just create a db.json file inside the data folder and put the following data in it.

{
  "solutions": [
    {"id": 1, "word": "ninja"},
    {"id": 2, "word": "spade"},
    {"id": 3, "word": "pools"},
    {"id": 4, "word": "drive"},
    {"id": 5, "word": "relax"},
    {"id": 6, "word": "times"},
    {"id": 7, "word": "train"},
    {"id": 8, "word": "cores"},
    {"id": 9, "word": "pours"},
    {"id": 10, "word": "blame"},
    {"id": 11, "word": "banks"},
    {"id": 12, "word": "phone"},
    {"id": 13, "word": "bling"},
    {"id": 14, "word": "coins"},
    {"id": 15, "word": "hello"}
  ]
}

Install json-server using npm i json-server to fetch data inside React component. The json-server turns JSON data into API endpoints. Run json-server using the command json-server ./data/db.json --port 3001. It'll provide the endpoint http://localhost:3001/solutions to access the above data. We'll use the useEffect() hook to fetch data from json-server. Also, we need the useState() hook to store data from json-server. Now, the App.js file will look like this:

//   src/App.js

import { useEffect, useState } from 'react'

function App() {
  const [solution, setSolution] = useState(null)

  useEffect(() => {
    fetch('http://localhost:3001/solutions')
      .then(res => res.json())
      .then(json => {
        // random int between 0 & 14
        const randomSolution = json[Math.floor(Math.random() * json.length)]
        setSolution(randomSolution.word)
      })
  }, [setSolution])

  return (
    <div className="App">
      <h1>Wordle</h1>
      {solution && <div>Solution is: {solution}</div>}
    </div>
  )
}
export default App

Make a Wordle Hook

Now, you've to write game logic to track user guesses. You'll use letter colorizing to check if the guess is correct. You need to make a custom Wordle hook to handle the game logic. We'll use that hook to implement the functionality. In this way, we'll keep the UI of the game separate from the logic. Create a useWordle.js file inside the src/hooks folder. Let's add a skeleton logic in the useWordle.js file.

// src/hooks/useWordle.js

import { useState } from 'react'

const useWordle = (solution) => {
  const [turn, setTurn] = useState(0) 
  const [currentGuess, setCurrentGuess] = useState('')
  const [guesses, setGuesses] = useState([]) // each guess is an array
  const [history, setHistory] = useState([]) // each guess is a string
  const [isCorrect, setIsCorrect] = useState(false)

  // format a guess into an array of letter objects 
  // e.g. [{key: 'a', color: 'yellow'}]
  const formatGuess = () => {

  }

  // add a new guess to the guesses state
  // update the isCorrect state if the guess is correct
  // add one to the turn state
  const addNewGuess = () => {

  }

  // handle keyup event & track current guess
  // if user presses enter, add the new guess
  const handleKeyup = () => {

  }

  return {turn, currentGuess, guesses, isCorrect, handleKeyup}
}

export default useWordle

Track the Current Guess

You need to track the guess while the user submits the word. For this, we'll use an event listener for every key press. We'll build a new Wordle React component to set up this listener. We'll also access the useWordle() hook inside the Wordle component.

// src/components/Wordle.js

import React, { useEffect } from 'react'
import useWordle from '../hooks/useWordle'

export default function Wordle({ solution }) {
  const { currentGuess, handleKeyup } = useWordle(solution)

  useEffect(() => {
    window.addEventListener('keyup', handleKeyup)

    return () => window.removeEventListener('keyup', handleKeyup)
  }, [handleKeyup])

  return (
    <div>
      <div>Current Guess - {currentGuess}</div>
    </div>
  )
}

For every key press, handleKeyup function will get fired. So, you need to make sure only English letters get tracked. Also, backspace will delete the last letter from the current guess.

const handleKeyup = ({ key }) => {
    if (key === 'Backspace') {
      setCurrentGuess(prev => prev.slice(0, -1))
      return
    }
    if (/^[A-Za-z]$/.test(key)) {
      if (currentGuess.length < 5) {
        setCurrentGuess(prev => prev + key)
      }
    }
  }

We're going to add UI in the Wordle component. So, we need to update App component according to it.

import Wordle from './components/Wordle'

return (
    <div className="App">
      <h1>Wordle (Lingo)</h1>
      {solution && <Wordle solution={solution} />}
    </div>
)

Submit and Format the Guesses

When a user submits the word by pressing Enter, we need to handle this word for further game logic. We'll build the following logic:

  • Only add a guess if the turn is less than five.

  • Don't allow duplicate words.

  • Check word is five chars long.

Format guess functionality will compare each letter against the solution word and apply colors accordingly.

const formatGuess = () => {
    let solutionArray = [...solution]
    let formattedGuess = [...currentGuess].map((l) => {
      return {key: l, color: 'grey'}
    })

    // find any green letters
    formattedGuess.forEach((l, i) => {
      if (solution[i] === l.key) {
        formattedGuess[i].color = 'green'
        solutionArray[i] = null
      }
    })

    // find any yellow letters
    formattedGuess.forEach((l, i) => {
      if (solutionArray.includes(l.key) && l.color !== 'green') {
        formattedGuess[i].color = 'yellow'
        solutionArray[solutionArray.indexOf(l.key)] = null
      }
    })

    return formattedGuess
}

Also, you need to call formatGuess() inside handleKeyup() when the user presses Enter.

const handleKeyup = ({ key }) => {
    if (key === 'Enter') {
      // only add guess if turn is less than 5
      if (turn > 5) {
        console.log('you used all your guesses!')
        return
      }
      // do not allow duplicate words
      if (history.includes(currentGuess)) {
        console.log('you already tried that word.')
        return
      }
      // check word is 5 chars
      if (currentGuess.length !== 5) {
        console.log('word must be 5 chars.')
        return
      }
      const formatted = formatGuess()
      console.log(formatted)
    }
    if (key === 'Backspace') {
      setCurrentGuess(prev => prev.slice(0, -1))
      return
    }
    if (/^[A-Za-z]$/.test(key)) {
      if (currentGuess.length < 5) {
        setCurrentGuess(prev => prev + key)
      }
    }
}

return {turn, currentGuess, guesses, isCorrect, handleKeyup}

Add the New Guesses

Now, you've done guess tracking, guess submitting, and guess formatting. You must add a formatted guess in the guesses array of the useState() hook. After that, we'll print these guesses onto the Wordle grid. In the handleKeyup method, we need to call addNewGuess() after formatGuess().

  const formatted = formatGuess()
  addNewGuess(formatted)

We need a list of six guesses to print on the game grid. So, we've to set the guesses' length to six in useWordle.js.

const [guesses, setGuesses] = useState([...Array(6)])

Let's implement the addNewGuess() to update the guesses list for the Wordle grid.

const addNewGuess = (formattedGuess) => {
    if (currentGuess === solution) {
      setIsCorrect(true)
    }
    setGuesses(prevGuesses => {
      let newGuesses = [...prevGuesses]
      newGuesses[turn] = formattedGuess
      return newGuesses
    })
    setHistory(prevHistory => {
      return [...prevHistory, currentGuess]
    })
    setTurn(prevTurn => {
      return prevTurn + 1
    })
    setCurrentGuess('')
}

Also, we need all these values inside the Wordle component.
const { currentGuess, guesses, turn, isCorrect, handleKeyup } = useWordle(solution)

Create a Game Grid

Now, We've to display guesses on the Wordle grid. We'll create a grid of six rows. Each row will consist of five squares for five letters. So, we'll create two components, Grid and Row. Let's create the Grid component first.

// src/components/Grid.js

import React from 'react'
import Row from './Row'

export default function Grid({ guesses, currentGuess, turn }) {
  return (
    <div>
      {guesses.map((g, i) => {
        return <Row key={i} /> 
      })}
    </div>
  )
}

We must create a Row component for the Grid component.

// src/components/Row.js

import React from 'react'

export default function Row() {

  return (
    <div className="row">
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
    </div>
  )

}

Let's add styles in the index.css file for the Row Component.

.row {
  text-align: center;
  display: flex;
  justify-content: center;
}
.row > div {
  display: block;
  width: 60px;
  height: 60px;
  border: 1px solid #bbb;
  margin: 4px;
  text-align: center;
  line-height: 60px;
  text-transform: uppercase;
  font-weight: bold;
  font-size: 2.5em;
}

Finally, add the Grid component to the Wordle component.

... 
import Grid from './Grid'
...
return (
    <div>
      ...
      <Grid guesses={guesses} currentGuess={currentGuess} turn={turn} />
    </div>
  )

Game Grid

Show the Past and Current Guesses

At the moment, no letter is displayed on the grid. We'll display the guesses' list on this grid. First, we'll pass each guess to Row. All letters of guess will get displayed on row squares. Every square will have a background color according to the guess. Let's pass a guess to each row inside the Grid component.

// src/components/Grid.js

export default function Grid({ guesses, currentGuess, turn }) {
  return (
    <div>
      {guesses.map((g, i) => {
        return <Row key={i} guess={g} /> 
      })}
    </div>
  )
}

Also, we need to adjust the logic of the Row component for the guesses display. Now the row component will look like this.

// src/components/Row.js

import React from 'react'

export default function Row({ guess }) {

  if (guess) {
    return (
      <div className="row past">
        {guess.map((l, i) => (
          <div key={i} className={l.color}>{l.key}</div>
        ))}
      </div>
    )
  }

  return (
    <div className="row">
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
    </div>
  )

}

Add the following styles for squares to the index.css file.

.row > div.green {
  background: #5ac85a;
  border-color: #5ac85a;
}
.row > div.grey {
  background: #a1a1a1;
  border-color: #a1a1a1;
}
.row > div.yellow {
  background: #e2cc68;
  border-color: #e2cc68;
}

We're displaying all the past guesses on the grid. We need to display the current guess while typing as well. In the Grid component, we want to pass the current guess to the Row component. So, the row with the current turn will display the current guess.

// src/components/Grid.js

return (
    <div>
      {guesses.map((g, i) => {
        if (turn === i) {
          return <Row key={i} currentGuess={currentGuess} />
        }
        return <Row key={i} guess={g} /> 
      })}
    </div>
)

Now, update the logic of the Row component for the current guess. Also, extract the current guess as a prop in the Row component.

// src/components/Row.js
if (currentGuess) {
    let letters = currentGuess.split('')

    return (
      <div className="row current">
        {letters.map((letter, i) => (
          <div key={i} className="filled">{letter}</div>
        ))}
        {[...Array(5 - letters.length)].map((_,i) => (
          <div key={i}></div>
        ))}
      </div>
    )
}

Now, we can display the current guess and past guesses.

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

OpenReplay

Start enjoying your debugging experience - start using OpenReplay for free.

Tile Animations

There're a few more things we need to do. First of all, we'll handle animations for letters and words. We'll do this through keyframe animations. We'll not discuss animations in detail, as our focus is building a Wordle game using React. Let's add animations to the index.css file.

.row > div.green {
  --background: #5ac85a;
  --border-color: #5ac85a;
  animation: flip 0.5s ease forwards;
}
.row > div.grey {
  --background: #a1a1a1;
  --border-color: #a1a1a1;
  animation: flip 0.6s ease forwards;
}
.row > div.yellow {
  --background: #e2cc68;
  --border-color: #e2cc68;
  animation: flip 0.5s ease forwards;
}
.row > div:nth-child(2) {
  animation-delay: 0.2s;
}
.row > div:nth-child(3) {
  animation-delay: 0.4s;
}
.row > div:nth-child(4) {
  animation-delay: 0.6s;
}
.row > div:nth-child(5) {
  animation-delay: 0.8s;
}

/* keyframe animations */
@keyframes flip {
  0% {
    transform: rotateX(0);
    background: #fff;
    border-color: #333;
  }
  45% {
    transform: rotateX(90deg);
    background: white;
    border-color: #333;
  }
  55% {
    transform: rotateX(90deg);
    background: var(--background);
    border-color: var(--border-color);
  }
  100% {
    transform: rotateX(0deg);
    background: var(--background);
    border-color: var(--border-color);
    color: #eee;
  }
}

We also need to add a bounce effect to squares of the current guess. Let's add this bounce animation to the index.css file.

.row.current > div.filled {
  animation: bounce 0.2s ease-in-out forwards;
}

@keyframes bounce {
  0% { 
    transform: scale(1);
    border-color: #ddd;
  }
  50% { 
    transform: scale(1.2);
  }
  100% {
    transform: scale(1);
    border-color: #333;
  }
}

Now, We're done with the animations part as well.

Tile Animations

Keypad Component

We've to build a little keypad to show the letters used and matched. It's basically to visualize what letters we've used. Let's first create letters data for the keypad.

//  src/constants/keys.js

  const keys = [
    {key: "a"}, {key: "b"}, {key: "c"}, {key: "d"}, {key: "e"},
    {key: "f"}, {key: "g"}, {key: "h"}, {key: "i"}, {key: "j"},
    {key: "k"}, {key: "l"}, {key: "m"}, {key: "n"}, {key: "o"},
    {key: "p"}, {key: "q"}, {key: "r"}, {key: "s"}, {key: "t"},
    {key: "u"}, {key: "v"}, {key: "w"}, {key: "x"}, {key: "y"},
    {key: "z"},
  ];

export default keys;

Now, we've to create a Keypad component.

//  src/components/Keypad.js

import React, { useState, useEffect } from 'react'

export default function Keypad({keys}) {
  const [letters, setLetters] = useState(null)

  useEffect(() => {
    setLetters(keys)
  }, [keys])

  return (
    <div className="keypad">
      {letters && letters.map(l => {
        return (
          <div key={l.key}>{l.key}</div>
        )
      })}
    </div>
  )
}

Let's add styles to index.css for the Keypad component.

.keypad {
  max-width: 500px;
  margin: 20px auto;
}
.keypad > div {
  margin: 5px;
  width: 40px;
  height: 50px;
  background: #eee;
  display: inline-block;
  border-radius: 6px;
  line-height: 50px;
}

Now, you need to add the Keypad component to the Wordle component.

import Keypad from './Keypad'
import keys from '../constants/keys'

return (
    <div>
      ...
      <Keypad keys={keys}/>
    </div>
)

At the moment, the keypad's not reflecting the keys used. We've to update it inside useWordle.js.

// src/hooks/useWordle.js
const [usedKeys, setUsedKeys] = useState({}) 

const addNewGuess = (formattedGuess) => {
    if (currentGuess === solution) {
      setIsCorrect(true)
    }
    setGuesses(prevGuesses => {
      let newGuesses = [...prevGuesses]
      newGuesses[turn] = formattedGuess
      return newGuesses
    })
    setHistory(prevHistory => {
      return [...prevHistory, currentGuess]
    })
    setTurn(prevTurn => {
      return prevTurn + 1
    })
    setUsedKeys(prevUsedKeys => {
      formattedGuess.forEach(l => {
        const currentColor = prevUsedKeys[l.key]

        if (l.color === 'green') {
          prevUsedKeys[l.key] = 'green'
          return
        }
        if (l.color === 'yellow' && currentColor !== 'green') {
          prevUsedKeys[l.key] = 'yellow'
          return
        }
        if (l.color === 'grey' && currentColor !== ('green' || 'yellow')) {
          prevUsedKeys[l.key] = 'grey'
          return
        }
      })

      return prevUsedKeys
    })
    setCurrentGuess('')
}

  return {turn, currentGuess, guesses, isCorrect, usedKeys, handleKeyup}

Also, update the Wordle component.

const { currentGuess, guesses, turn, isCorrect, usedKeys, handleKeyup } = useWordle(solution)

<Keypad keys={keys} usedKeys={usedKeys}/>

Now, handle the usedKeys prop inside the Keypad component and reflect used key colors on the keypad.

export default function Keypad({ keys, usedKeys }) {
  return (
    <div className="keypad">
      {letters && letters.map(l => {
        const color = usedKeys[l.key]
        return (
          <div key={l.key} className={color}>{l.key}</div>
        )
      })}
    </div>
  )
}

We must add styles to index.css for the Keypad component.

.keypad > div.green {
  background: #5ac85a;
  color: #fff;
  transition: all 0.3s ease-in;
}
.keypad > div.yellow {
  background: #e2cc68;
  color: #fff;
  transition: all 0.3s ease-in;
}
.keypad > div.grey {
  background: #a1a1a1;
  color: #fff;
  transition: all 0.3s ease-in;
}

Now, we're done with the Keypad component.

Keypad Component

Showing a Modal

Finally, we need to detect when to end the game. We've to handle two scenarios:

  • When the user guesses correctly.

  • When the user runs out of turns.

For this, we'll add an event listener inside the useEffect() hook of Wordle component.

useEffect(() => {
    window.addEventListener('keyup', handleKeyup)

    if (isCorrect) {
      window.removeEventListener('keyup', handleKeyup)
    }
    if (turn > 5) {
      window.removeEventListener('keyup', handleKeyup)
    }

    return () => window.removeEventListener('keyup', handleKeyup)
  }, [handleKeyup, isCorrect, turn])

Now, we've to make a modal appear on the screen when the game ends. Let's create a Modal component.

// src/components/Modal.js

import React from 'react'

export default function Modal({ isCorrect, solution, turn }) {
  return (
    <div className="modal">
      {isCorrect && (
        <div>
          <h1>You Win!</h1>
          <p className="solution">{solution}</p>
          <p>You found the word in {turn} guesses</p>
        </div>
      )}
      {!isCorrect && (
        <div>
          <h1>Unlucky!</h1>
          <p className="solution">{solution}</p>
          <p>Better luck next time</p>
        </div>
      )}
    </div>
  )
}

Also, add styles to index.css for the Modal component.

.modal {
  background: rgba(255,255,255,0.7);
  position: fixed;
  width: 100%;
  height: 100%;
  top: 0;
  left: 0;
}
.modal div {
  max-width: 480px;
  background: #fff;
  padding: 40px;
  border-radius: 10px;
  margin: 10% auto;
  box-shadow: 2px 2px 10px rgba(0,0,0,0.3);
}
.modal .solution {
  border: 1px solid MediumSeaGreen;
  color: #fff;
  background-color: MediumSeaGreen;
  font-weight: bold;
  font-size: 2.5rem;
  text-transform: uppercase;
  letter-spacing: 1px;
}

We need to add the Modal component inside the Wordle component.

import React, { useState, useEffect } from 'react'

import Modal from './Modal'

const [showModal, setShowModal] = useState(false)

useEffect(() => {
    window.addEventListener('keyup', handleKeyup)

    if (isCorrect) {
      setTimeout(() => setShowModal(true), 2000)
      window.removeEventListener('keyup', handleKeyup)
    }
    if (turn > 5) {
      setTimeout(() => setShowModal(true), 2000)
      window.removeEventListener('keyup', handleKeyup)
    }

    return () => window.removeEventListener('keyup', handleKeyup)
  }, [handleKeyup, isCorrect, turn])

  {showModal && <Modal isCorrect={isCorrect} turn={turn} solution={solution} />}

Show Modal

A modal will appear on the screen when the game ends.

You're done creating a Wordle clone using React!

newsletter