DEV Community

Cover image for Make your own Wordle in React πŸŸ©πŸŸ¨β¬›οΈ
Kris Gardiner
Kris Gardiner

Posted on

Make your own Wordle in React πŸŸ©πŸŸ¨β¬›οΈ

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

πŸš€ Let's build it!

This project uses:
#react
#styled-components

🎨 Basic Styling and Layout

1) We need a header!
Wordle Header

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;
Enter fullscreen mode Exit fullscreen mode

2) Next is the guesses section. Each guess is 5 letters long and there's a total of 6 tries.
5x6 grid of guesses

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;
Enter fullscreen mode Exit fullscreen mode

3) Last is the keyboard UI.
Wordle keyboard UI

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;
Enter fullscreen mode Exit fullscreen mode

Initial keyboard UI

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.

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>
Enter fullscreen mode Exit fullscreen mode

Keyboard UI with space in second row

3b) Something still doesn't seem quite right.. We need to make the Enter and Backspace keys bigger!

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>
Enter fullscreen mode Exit fullscreen mode

3c) One last touch here, the backspace icon!

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>
Enter fullscreen mode Exit fullscreen mode

Finished keyboard UI

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

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;
Enter fullscreen mode Exit fullscreen mode

Finished wordle layout and styling


🧐 Building the Logic

1) Let's start off nice and easy. Capture the mouse clicks from each keyboard UI button.

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

2a) Now let's apply this single source of truth to our keydown event listener.

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);
    };
  }, []);

...
Enter fullscreen mode Exit fullscreen mode

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


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>
Enter fullscreen mode Exit fullscreen mode

4) Next, keyboard events, mouse clicks need to update the guesses state.

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);
   }, []);

...
Enter fullscreen mode Exit fullscreen mode

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.

...
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;
   }
};
Enter fullscreen mode Exit fullscreen mode

Capturing mouse clicks and keyboard events for wordle game

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.

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;
`;
Enter fullscreen mode Exit fullscreen mode

6) Can't forget to display the hints for all the letters!

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;
};
Enter fullscreen mode Exit fullscreen mode

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.

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);
}, []);
Enter fullscreen mode Exit fullscreen mode

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 to show a popup when you correctly guess the word. It will need a button to share the completed game.

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;
Enter fullscreen mode Exit fullscreen mode

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. Feel free to open a PR, submit an issue to see how I would build it, or fork it and make it your own!


Follow me on Twitter (@krisgardiner)

Top comments (0)