Make your own Wordle in React 🟩🟨⬛️
Tags: react, tutorial, gamedev, styledcomponents
Published: 2022-03-07 04:43:42

Wordle is a web-based word game created and developed by Welsh software engineer Josh Wardle that went viral and caught the attention of The New York Times leading to them buying it for more than $1 million! 

# 👀 Some interesting facts about the game:

- From its initial release, it went from 90 to 300,000 users in 2 months
- The original list of 12,000 five letter word of the days was narrowed down to 2,500.
- Sharing the grid of green, yellow, and black squares was released after Josh discovered his users were manually typing it out to share with others.


# 📏 The rules of the game are simple!

1. Guess the WORDLE in 6 tries.
2. Each guess must be a valid 5 letter word. Hit the enter button to submit.
3. After each guess, the color of the tiles will change to show how close your guess was to the word.
![Green square means letter is in the right spot, yellow square means letter is in the word but in the wrong spot, gray means letter is not in the word](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/03jnst2tm31104djehei.png)

___

# 🚀 Let's build it!

This project uses:
[#react](https://reactjs.org/)
[#styled-components](https://styled-components.com/)

## 🎨 Basic Styling and Layout

1) We need a header!
![Wordle Header](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hmze0j5kjwpr5uag0t5v.png)
```javascript
import styled from "styled-components";
import "./App.css";

const Main = styled.main`
  font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif;

  display: flex;
  flex-direction: column;
  align-items: center;

  width: 100%;
  max-width: 500px;
  margin: 0 auto;
`;

const Header = styled.header`
  display: flex;
  justify-content: center;
  align-items: center;
  height: 50px;
  width: 100%;

  border-bottom: 1px solid #3a3a3c;

  font-weight: 700;
  font-size: 3.6rem;
  letter-spacing: 0.2rem;
  text-transform: uppercase;
`;

function App() {
  return (
    <Main>
      <Header>WORDLE</Header>
    </Main>
  );
}

export default App;
```

2) Next is the guesses section. Each guess is 5 letters long and there's a total of 6 tries. 
![5x6 grid of guesses](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4gvzymba5k97z1glhn8i.png)
 
```javascript
import styled from "styled-components";
import "./App.css";

const Main = styled.main`
  font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif;

  display: flex;
  flex-direction: column;
  align-items: center;

  width: 100%;
  height: 100%;
  max-width: 500px;
  margin: 0 auto;
`;

const Header = styled.header`
  display: flex;
  justify-content: center;
  align-items: center;
  height: 50px;
  width: 100%;

  border-bottom: 1px solid #3a3a3c;

  font-weight: 700;
  font-size: 3.6rem;
  letter-spacing: 0.2rem;
  text-transform: uppercase;
`;

const GameSection = styled.section`
  display: flex;
  justify-content: center;
  align-items: center;
  flex-grow: 1;
`;

const TileContainer = styled.div`
  display: grid;
  grid-template-rows: repeat(6, 1fr);
  grid-gap: 5px;

  height: 420px;
  width: 350px;
`;

const TileRow = styled.div`
  width: 100%;

  display: grid;
  grid-template-columns: repeat(5, 1fr);
  grid-gap: 5px;
`;

const Tile = styled.div`
  display: inline-flex;
  justify-content: center;
  align-items: center;

  border: 2px solid #3a3a3c;
  font-size: 3.2rem;
  font-weight: bold;
  line-height: 3.2rem;
  text-transform: uppercase;
`;

function App() {
  return (
    <Main>
      <Header>WORDLE</Header>
      <GameSection>
        <TileContainer>
          {[0, 1, 2, 3, 4, 5].map((i) => (
            <TileRow key={i}>
              {[0, 1, 2, 3, 4].map((i) => (
                <Tile key={i}></Tile>
              ))}
            </TileRow>
          ))}
        </TileContainer>
      </GameSection>
    </Main>
  );
}

export default App;
```

3) Last is the keyboard UI.
![Wordle keyboard UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qf8qqa1t3p4y5qbk9g3u.png)
 
```javascript
import styled from "styled-components";
import "./App.css";

const Main = styled.main`
  font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif;

  display: flex;
  flex-direction: column;
  align-items: center;

  width: 100%;
  height: 100%;
  max-width: 500px;
  margin: 0 auto;
`;

const Header = styled.header`
  display: flex;
  justify-content: center;
  align-items: center;
  height: 50px;
  width: 100%;

  border-bottom: 1px solid #3a3a3c;

  font-weight: 700;
  font-size: 3.6rem;
  letter-spacing: 0.2rem;
  text-transform: uppercase;
`;

const GameSection = styled.section`
  display: flex;
  justify-content: center;
  align-items: center;
  flex-grow: 1;
`;
const TileContainer = styled.div`
  display: grid;
  grid-template-rows: repeat(6, 1fr);
  grid-gap: 5px;

  height: 420px;
  width: 350px;
`;
const TileRow = styled.div`
  width: 100%;

  display: grid;
  grid-template-columns: repeat(5, 1fr);
  grid-gap: 5px;
`;
const Tile = styled.div`
  display: inline-flex;
  justify-content: center;
  align-items: center;

  border: 2px solid #3a3a3c;
  font-size: 3.2rem;
  font-weight: bold;
  line-height: 3.2rem;
  text-transform: uppercase;

  user-select: none;
`;

const KeyboardSection = styled.section`
  height: 200px;
  width: 100%;
  display: flex;
  flex-direction: column;
`;

const KeyboardRow = styled.div`
  width: 100%;
  margin: 0 auto 8px;

  display: flex;
  align-items: center;
  justify-content: space-around;
`;

const KeyboardButton = styled.button`
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 0;
  margin: 0 6px 0 0;
  height: 58px;
  flex: 1;

  border: 0;
  border-radius: 4px;
  background-color: #818384;
  font-weight: bold;
  text-transform: uppercase;
  color: #d7dadc;

  cursor: pointer;
  user-select: none;

  &:last-of-type {
    margin: 0;
  }
`;

function App() {
  return (
    <Main>
      <Header>WORDLE</Header>
      <GameSection>
        <TileContainer>
          {[0, 1, 2, 3, 4, 5].map((i) => (
            <TileRow key={i}>
              {[0, 1, 2, 3, 4].map((i) => (
                <Tile key={i}></Tile>
              ))}
            </TileRow>
          ))}
        </TileContainer>
      </GameSection>
      <KeyboardSection>
        <KeyboardRow>
          {["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"].map((key) => (
            <KeyboardButton>{key}</KeyboardButton>
          ))}
        </KeyboardRow>
        <KeyboardRow>
          {["a", "s", "d", "f", "g", "h", "j", "k", "l"].map((key) => (
            <KeyboardButton>{key}</KeyboardButton>
          ))}
        </KeyboardRow>
        <KeyboardRow>
          {["enter", "z", "x", "c", "v", "b", "n", "m", "backspace"].map(
            (key) => (
              <KeyboardButton>{key}</KeyboardButton>
            )
          )}
        </KeyboardRow>
      </KeyboardSection>
    </Main>
  );
}

export default App;
```
![Initial keyboard UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f6qfcafipp5nt52mw6vp.png)
 

3a) There's a little issue with the layout here, the second row needs some space on the sides. So let's create a utility layout component just for extra space.
```javascript
const Flex = styled.div`
  ${({ item }) => `flex: ${item};`}
`;
...
<KeyboardRow>
  <Flex item={0.5} />
  {["a", "s", "d", "f", "g", "h", "j", "k", "l"].map((key) => (
    <KeyboardButton>{key}</KeyboardButton>
  ))}
  <Flex item={0.5} />
</KeyboardRow>
```
![Keyboard UI with space in second row](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wr5dvy4w99tpm6rr11zb.png)
 
3b) Something still doesn't seem quite right.. We need to make the `Enter` and `Backspace` keys bigger! 
```javascript
const KeyboardButton = styled.button`
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 0;
  margin: 0 6px 0 0;
  height: 58px;
	${({ item }) => (item ? `flex: ${item};` : `flex: 1;`)}

  border: 0;
  border-radius: 4px;
  background-color: #818384;
  font-weight: bold;
  text-transform: uppercase;
  color: #d7dadc;

  cursor: pointer;
  user-select: none;

  &:last-of-type {
    margin: 0;
  }
`;
...
<KeyboardRow>
  {["enter", "z", "x", "c", "v", "b", "n", "m", "backspace"].map(
    (key) => (
      <KeyboardButton
        flex={["enter", "backspace"].includes(key) ? 1.5 : 1}
      >
        {key}
      </KeyboardButton>
    )
  )}
</KeyboardRow>
```

3c) One last touch here, the backspace icon!
```javascript
const BackspaceIcon = () => (
  <svg
    xmlns="http://www.w3.org/2000/svg"
    height="24"
    viewBox="0 0 24 24"
    width="24"
  >
    <path
      fill="#d7dadc"
      d="M22 3H7c-.69 0-1.23.35-1.59.88L0 12l5.41 8.11c.36.53.9.89 1.59.89h15c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H7.07L2.4 12l4.66-7H22v14zm-11.59-2L14 13.41 17.59 17 19 15.59 15.41 12 19 8.41 17.59 7 14 10.59 10.41 7 9 8.41 12.59 12 9 15.59z"
    ></path>
  </svg>
);
...
<KeyboardRow>
  {["enter", "z", "x", "c", "v", "b", "n", "m", "backspace"].map(
    (key) => (
      <KeyboardButton
        flex={["enter", "backspace"].includes(key) ? 1.5 : 1}
      >
        {key === "backspace" ? <BackspaceIcon /> : key}
      </KeyboardButton>
    )
  )}
</KeyboardRow>
```
![Finished keyboard UI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w5sjlivxj1kbmy2z0dpb.png)
 

4) All done here! Let's abstract the styled-components into their own file so we can focus on the logic.

```javascript
import {
  Main,
  Header,
  GameSection,
  TileContainer,
  TileRow,
  Tile,
  KeyboardSection,
  KeyboardRow,
  KeyboardButton,
  Flex,
} from "./styled";
import { BackspaceIcon } from "./icons";
import "./App.css";

function App() {
  return (
    <Main>
      <Header>WORDLE</Header>
      <GameSection>
        <TileContainer>
          {[0, 1, 2, 3, 4, 5].map((i) => (
            <TileRow key={i}>
              {[0, 1, 2, 3, 4].map((i) => (
                <Tile key={i}></Tile>
              ))}
            </TileRow>
          ))}
        </TileContainer>
      </GameSection>
      <KeyboardSection>
        <KeyboardRow>
          {["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"].map((key) => (
            <KeyboardButton>{key}</KeyboardButton>
          ))}
        </KeyboardRow>
        <KeyboardRow>
          <Flex item={0.5} />
          {["a", "s", "d", "f", "g", "h", "j", "k", "l"].map((key) => (
            <KeyboardButton>{key}</KeyboardButton>
          ))}
          <Flex item={0.5} />
        </KeyboardRow>
        <KeyboardRow>
          {["enter", "z", "x", "c", "v", "b", "n", "m", "backspace"].map(
            (key) => (
              <KeyboardButton
                flex={["enter", "backspace"].includes(key) ? 1.5 : 1}
              >
                {key === "backspace" ? <BackspaceIcon /> : key}
              </KeyboardButton>
            )
          )}
        </KeyboardRow>
      </KeyboardSection>
    </Main>
  );
}

export default App;
``` 
![Finished wordle layout and styling](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x56eafg0v4254dz8ylne.png)
 
___

## 🧐 Building the Logic

1) Let's start off nice and easy. Capture the mouse clicks from each keyboard UI button.
```javascript
function App() {
  const handleClick = (key) => {};

  return (
    <Main>
      <Header>WORDLE</Header>
      <GameSection>
        <TileContainer>
          {[0, 1, 2, 3, 4, 5].map((i) => (
            <TileRow key={i}>
              {[0, 1, 2, 3, 4].map((i) => (
                <Tile key={i}></Tile>
              ))}
            </TileRow>
          ))}
        </TileContainer>
      </GameSection>
      <KeyboardSection>
        <KeyboardRow>
          {["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"].map((key) => (
            <KeyboardButton onClick={() => handleClick(key)}>
              {key}
            </KeyboardButton>
          ))}
        </KeyboardRow>
        <KeyboardRow>
          <Flex item={0.5} />
          {["a", "s", "d", "f", "g", "h", "j", "k", "l"].map((key) => (
            <KeyboardButton onClick={() => handleClick(key)}>
              {key}
            </KeyboardButton>
          ))}
          <Flex item={0.5} />
        </KeyboardRow>
        <KeyboardRow>
          {["enter", "z", "x", "c", "v", "b", "n", "m", "backspace"].map(
            (key) => (
              <KeyboardButton
                flex={["enter", "backspace"].includes(key) ? 1.5 : 1}
                onClick={() => handleClick(key)}
              >
                {key === "backspace" ? <BackspaceIcon /> : key}
              </KeyboardButton>
            )
          )}
        </KeyboardRow>
      </KeyboardSection>
    </Main>
  );
}
```

2) Now that we have mouse clicks and mobile taps registered, we have one more thing to account for.. Keyboard events! We only want to listen to the keys displayed on the keyboard so let’s reuse the arrays we used to display the keyboard buttons and create **one source of truth**.
```javascript
const keyboardRows = [
  ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
  ["a", "s", "d", "f", "g", "h", "j", "k", "l"],
  ["enter", "z", "x", "c", "v", "b", "n", "m", "backspace"],
];

function App() {
  const handleClick = (key) => {};

  useEffect(() => {
    window.addEventListener("keydown", (e) => {
      console.log(e.key);
    });
  }, []);

  return (
    <Main>
      <Header>WORDLE</Header>
      <GameSection>
        <TileContainer>
          {[0, 1, 2, 3, 4, 5].map((i) => (
            <TileRow key={i}>
              {[0, 1, 2, 3, 4].map((i) => (
                <Tile key={i}></Tile>
              ))}
            </TileRow>
          ))}
        </TileContainer>
      </GameSection>
      <KeyboardSection>
        {keyboardRows.map((keys, i) => (
          <KeyboardRow key={i}>
            {i === 1 && <Flex item={0.5} />}
            {keys.map((key) => (
              <KeyboardButton
                key={key}
                onClick={() => handleClick(key)}
                flex={["enter", "backspace"].includes(key) ? 1.5 : 1}
              >
                {key === "backspace" ? <BackspaceIcon /> : key}
              </KeyboardButton>
            ))}
            {i === 1 && <Flex item={0.5} />}
          </KeyboardRow>
        ))}
      </KeyboardSection>
    </Main>
  );
}
```

2a) Now let's apply this single source of truth to our `keydown` event listener.
```javascript
const keyboardRows = [
  ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
  ["a", "s", "d", "f", "g", "h", "j", "k", "l"],
  ["enter", "z", "x", "c", "v", "b", "n", "m", "backspace"],
];

const allKeys = keyboardRows.flat();

function App() {
  const handleClick = (key) => {};

  useEffect(() => {
    const handleKeyDown = (e) => {
      if (allKeys.includes(e.key)) {
        console.log(e.key);
      }
    };

    window.addEventListener("keydown", handleKeyDown);

    return () => {
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, []);

...
```

3) We need to keep track of which guess we're on and display the guesses in the game tiles.
```javascript

const wordLength = 5;
...
function App() {
  const [guesses, setGuesses] = useState({
  0: Array.from({ length: wordLength }).fill(""),
  1: Array.from({ length: wordLength }).fill(""),
  2: Array.from({ length: wordLength }).fill(""),
  3: Array.from({ length: wordLength }).fill(""),
  4: Array.from({ length: wordLength }).fill(""),
  5: Array.from({ length: wordLength }).fill(""),
});
...
<TileContainer>
  {Object.values(guesses).map((word, i) => (
    <TileRow key={i}>
      {word.map((letter, i) => (
        <Tile key={i}>{letter}</Tile>
      ))}
    </TileRow>
  ))}
</TileContainer>
```

4) Next, keyboard events, mouse clicks need to update the guesses state.
```javascript
function App() {
   ...
   let letterIndex = useRef(0);
   let round = useRef(0);

   const enterGuess = (pressedKey) => {
      if (pressedKey === "backspace") {
         erase();
      } else if (pressedKey !== "enter") {
         publish( pressedKey );
      }
  };

   const erase = () => {
      const _letterIndex = letterIndex.current;
      const _round = round.current;

      setGuesses((prev) => {
        const newGuesses = { ...prev };
        newGuesses[_round][_letterIndex - 1] = "";
        return newGuesses;
      });

      letterIndex.current = _letterIndex - 1;
   };
   
   const publish = ( pressedKey ) => {
      const _letterIndex = letterIndex.current;
      const _round = round.current;
      
      setGuesses((prev) => {
        const newGuesses = { ...prev };
        newGuesses[_round][_letterIndex] = pressedKey.toLowerCase();
        return newGuesses;
      });

      letterIndex.current = _letterIndex + 1;
   };
   
   const handleClick = (key) => {
      const pressedKey = key.toLowerCase();

      enterGuess(pressedKey);
   };
   
   const handleKeyDown = (e) => {
      const pressedKey = e.key.toLowerCase();

      if (allKeys.includes(pressedKey)) {
         enterGuess(pressedKey);
      }
   };

   useEffect(() => {
      document.addEventListener("keydown", handleKeyDown);

      return () => document.removeEventListener("keydown", handleKeyDown);
   }, []);

...
```

4a) 🐛 There's a bug here! We need to add limitations when we’re at the first letter of a guess and a user presses backspace. Same thing when we’re on the last letter of a guess and the user continues to guess.
```javascript
...
const erase = () => {
   const _letterIndex = letterIndex.current;
   const _round = round.current;

   if (_letterIndex !== 0) {
      setGuesses((prev) => {
         const newGuesses = { ...prev };
         newGuesses[_round][_letterIndex - 1] = "";
         return newGuesses;
      });

      letterIndex.current = _letterIndex - 1;
   }
};

const publish = (pressedKey) => {
   const _letterIndex = letterIndex.current;
   const _round = round.current;

   if (_letterIndex < wordLength) {
      setGuesses((prev) => {
         const newGuesses = { ...prev };
         newGuesses[_round][_letterIndex] = pressedKey.toLowerCase();
         return newGuesses;
      });

      letterIndex.current = _letterIndex + 1;
   }
};
```
![Capturing mouse clicks and keyboard events for wordle game](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lg4qzkaihge7r7dmq702.gif)

5) This is huge progress, we're almost at the finish line! We need to verify the guess matches the word of the day on `Enter` and proceed to the next round of guesses.
```javascript
const wordOfTheDay = 'hello';
const [guesses, setGuesses] = useState({
   0: Array.from({ length: wordLength }).fill(""),
   1: Array.from({ length: wordLength }).fill(""),
   2: Array.from({ length: wordLength }).fill(""),
   3: Array.from({ length: wordLength }).fill(""),
   4: Array.from({ length: wordLength }).fill(""),
   5: Array.from({ length: wordLength }).fill(""),
});
const [markers, setMarkers] = useState({
   0: Array.from({ length: wordLength }).fill(""),
   1: Array.from({ length: wordLength }).fill(""),
   2: Array.from({ length: wordLength }).fill(""),
   3: Array.from({ length: wordLength }).fill(""),
   4: Array.from({ length: wordLength }).fill(""),
   5: Array.from({ length: wordLength }).fill(""),
});

...

const submit = () => {
	const _round = round.current;
  const updatedMarkers = {
    ...markers,
  };

  const tempWord = wordOfTheDay.split("");

  // Prioritize the letters in the correct spot
  tempWord.forEach((letter, index) => {
    const guessedLetter = guesses[round][index];

    if (guessedLetter === letter) {
      updatedMarkers[round][index] = "green";
      tempWord[index] = "";
    }
  });

  // Then find the letters in wrong spots
  tempWord.forEach((_, index) => {
    const guessedLetter = guesses[round][index];

    // Mark green when guessed letter is in the correct spot
    if (
      tempWord.includes(guessedLetter) &&
      index !== tempWord.indexOf(guessedLetter)
    ) {
      // Mark yellow when letter is in the word of the day but in the wrong spot
      updatedMarkers[round][index] = "yellow";
      tempWord[tempWord.indexOf(guessedLetter)] = "";
    }
  });

  setMarkers(updatedMarkers);
  round.current = _round + 1;
};

...

{Object.values(guesses).map((word, wordIndex) => (
   <TileRow key={wordIndex}>
      {word.map((letter, i) => (
         <Tile key={i} hint={markers[wordIndex][i]}>
            {letter}
         </Tile>
      ))}
   </TileRow>
))}

...

export const Tile = styled.div`
  display: inline-flex;
  justify-content: center;
  align-items: center;

  border: 2px solid #3a3a3c;
  font-size: 3.2rem;
  font-weight: bold;
  line-height: 3.2rem;
  text-transform: uppercase;

  ${({ hint }) => {
    console.log("hint:", hint, hint === "green", hint === "yellow");
    if (hint === "green") {
      return `background-color: #6aaa64;`;
    }
    if (hint === "yellow") {
      return `background-color: #b59f3b;`;
    }
  }}

  user-select: none;
`;
```

6) Can't forget to display the hints for all the letters!
```javascript
const submit = () => {
  const _round = round.current;

  const updatedMarkers = {
    ...markers,
  };

  const tempWord = wordOfTheDay.split("");

  const leftoverIndices = [];

  // Prioritize the letters in the correct spot
  tempWord.forEach((letter, index) => {
    const guessedLetter = guesses[_round][index];

    if (guessedLetter === letter) {
      updatedMarkers[_round][index] = "green";
      tempWord[index] = "";
    } else {
      // We will use this to mark other letters for hints
      leftoverIndices.push(index);
    }
  });

  // Then find the letters in wrong spots
  if (leftoverIndices.length) {
    leftoverIndices.forEach((index) => {
      const guessedLetter = guesses[_round][index];
      const correctPositionOfLetter = tempWord.indexOf(guessedLetter);

      if (
        tempWord.includes(guessedLetter) &&
        correctPositionOfLetter !== index
      ) {
        // Mark yellow when letter is in the word of the day but in the wrong spot
        updatedMarkers[_round][index] = "yellow";
        tempWord[correctPositionOfLetter] = "";
      } else {
        // This means the letter is not in the word of the day.
        updatedMarkers[_round][index] = "grey";
        tempWord[index] = "";
      }
    });
  }

  setMarkers(updatedMarkers);
  round.current = _round + 1;
  letterIndex.current = 0;
};
```

7) Good news, after that there is not much left to add except validation! We need to check if each guessed word is a valid word. Unfortunately, it'd be extremely difficult to manually do this so we need to leverage a dictionary API to do this for us.
```javascript
const fetchWord = (word) => {
  return fetch(`${API_URL}/${word}`, {
    method: "GET",
  })
    .then((res) => res.json())
    .then((res) => res)
    .catch((err) => console.log("err:", err));
};

const enterGuess = async (pressedKey) => {
   if (pressedKey === "enter" && !guesses[round.current].includes("")) {
      const validWord = await fetchWord(guesses[round.current].join(""));

      if (Array.isArray(validWord)) {
         submit();
      }
   } else if (pressedKey === "backspace") {
      erase();
   } else if (pressedKey !== "enter") {
      publish(pressedKey);
   }
};

const handleClick = (key) => {
   const pressedKey = key.toLowerCase();

   enterGuess(pressedKey);
};

useEffect(() => {
   const handleKeyDown = (e) => {
      const pressedKey = e.key.toLowerCase();

      if (allKeys.includes(pressedKey)) {
         enterGuess(pressedKey);
      }
   };

   document.addEventListener("keydown", handleKeyDown);

   return () => document.removeEventListener("keydown", handleKeyDown);
}, []);
```

8) 🏁 That's it, you made it. We're at the finish line! We need to check if the user guessed correctly and notify them when they win. We're going to use [react-modal](https://github.com/reactjs/react-modal) to show a popup when you correctly guess the word. It will need a button to share the completed game.

```javascript
function App() {
const [isModalVisible, setModalVisible] = useState(false);
const [isShared, setIsShared] = useState(false);

const win = () => {
   document.removeEventListener("keydown", handleKeyDown);
   setModalVisible(true);
};

const submit = () => {
    const _round = round.current;

    const updatedMarkers = {
      ...markers,
    };

    const tempWord = wordOfTheDay.split("");

    const leftoverIndices = [];

    // Prioritize the letters in the correct spot
    tempWord.forEach((letter, index) => {
      const guessedLetter = guesses[_round][index];

      if (guessedLetter === letter) {
        updatedMarkers[_round][index] = "green";
        tempWord[index] = "";
      } else {
        // We will use this to mark other letters for hints
        leftoverIndices.push(index);
      }
    });

    if (updatedMarkers[_round].every((guess) => guess === "green")) {
      setMarkers(updatedMarkers);
      win();
      return;
    }
...
};

const getDayOfYear = () => {
    const now = new Date();
    const start = new Date(now.getFullYear(), 0, 0);
    const diff = now - start;
    const oneDay = 1000 * 60 * 60 * 24;
    return Math.floor(diff / oneDay);
};

const copyMarkers = () => {
    let shareText = `Wordle ${getDayOfYear()}`;
    let shareGuesses = "";

    const amountOfGuesses = Object.entries(markers)
      .filter(([_, guesses]) => !guesses.includes(""))
      .map((round) => {
        const [_, guesses] = round;

        guesses.forEach((guess) => {
          if (guess === "green") {
            shareGuesses += "🟩";
          } else if (guess === "yellow") {
            shareGuesses += "🟨";
          } else {
            shareGuesses += "⬛️";
          }
        });

        shareGuesses += "\n";

        return "";
      });

    shareText += ` ${amountOfGuesses.length}/6\n${shareGuesses}`;

    navigator.clipboard.writeText(shareText); // NOTE: This doesn't work on mobile
    setIsShared(true);
};

...
return (
    <>
      <Main>
        <Header>WORDLE</Header>
        <GameSection>
          <TileContainer>
            {Object.values(guesses).map((word, wordIndex) => (
              <TileRow key={wordIndex}>
                {word.map((letter, i) => (
                  <Tile key={i} hint={markers[wordIndex][i]}>
                    {letter}
                  </Tile>
                ))}
              </TileRow>
            ))}
          </TileContainer>
        </GameSection>
        <KeyboardSection>
          {keyboardRows.map((keys, i) => (
            <KeyboardRow key={i}>
              {i === 1 && <Flex item={0.5} />}
              {keys.map((key) => (
                <KeyboardButton
                  key={key}
                  onClick={() => handleClick(key)}
                  flex={["enter", "backspace"].includes(key) ? 1.5 : 1}
                >
                  {key === "backspace" ? <BackspaceIcon /> : key}
                </KeyboardButton>
              ))}
              {i === 1 && <Flex item={0.5} />}
            </KeyboardRow>
          ))}
        </KeyboardSection>
      </Main>
      <div id="share">
        <Modal
          isOpen={isModalVisible}
          onRequestClose={() => setModalVisible(false)}
          style={{
            content: {
              top: "50%",
              left: "50%",
              right: "auto",
              bottom: "auto",
              marginRight: "-50%",
              transform: "translate(-50%, -50%)",
            },
          }}
          contentLabel="Share"
        >
          <ShareModal>
            <Heading>You win!</Heading>
            <Row>
              <h3>Show off your score</h3>
              <ShareButton onClick={copyMarkers} disabled={isShared}>
                {isShared ? "Copied!" : "Share"}
              </ShareButton>
            </Row>
          </ShareModal>
        </Modal>
      </div>
    </>
  );
}

export default App;
```

# Congratulations 🎉
You have just created your own Wordle game! I've intentionally left a bug with the share functionality so that you can spend some time improving the project. Hands on learning is always the best way to improve your skills.

___

# 🛠 Bug Fixes
- Support copy functionality on mobile devices
- Show guess hints on keyboard UI

# ✅ Bonus Ways to Improve
- Store daily progress of user by persisting data in local storage
- Track daily game stats from user and display in modal
- Fetch daily word via external API
- Animate the game interface with each user interaction
- Add dark mode
- Simplify the styled-components by applying themes

___

Would love to see how you would improve this on the [completed project](https://github.com/krisgardiner/wordle). Feel free to [open a PR](https://github.com/krisgardiner/wordle/pulls),  [submit an issue](https://github.com/krisgardiner/wordle/issues) to see how I would build it, or fork it and make it your own!

___

# Follow me on [Twitter (@krisgardiner)](https://twitter.com/krisgardiner)