Cup of Code

Consuming tech in an enjoyable way

Wordle in React: Picking Up Where We Left Off!

The flipping tiles are cool, but why stop there? In this tutorial, we’ll create all the other cool features that are in the original game!
Welcome to part 2 of my Wordle tutorial! In the previous blog post, I gave beginner’s tips and tricks in React, and we worked on creating guess distribution, landing page, and game index. You should start there because the features we will work on today depend on those!


In this blog post, we will continue developing the features that exist in the original game, but for some reason were left out of the common “Wordle in React” tutorials online.

I am also planning a last, but not least, part for this series — how to modify the game to play with words different than 5 letters long!

The agenda for today is:

  1. The share button
  2. A different view depends on the user’s game state
  3. The progress modal — adding streak!
  4. Bonus: A shaky row!
  5. Final touches


Are you ready? 😀


The Share Button

We are going back to the progress modal!

After they finish the game, our users want to show off their success and send their friends their game process. In the original game, when you click the share button, it copies to your clipboard a string of squares that are a visual representation of the guesses in the game (🟩⬛🟨). Then, you can paste that string anywhere you want: WhatsApp, Twitter, etc.

Users want to show off their success and send their friends their game process

We will build this feature in 2 steps: First, create the string of squares along with the game name, game number, and the number of guesses it took. Then, we add the “copy to clipboard” functionality.

1. Create The Squares String

There are two places you can create that squares-string in, and they both make sense. The first one is in the useWordle.js hook, right next to where we add a new guess to the guesses array: Adding a new guess— adding a new string of squares.

The second option is adding this code where the user requests the string: In the Share button’s “onClick” function. That way, we execute this code only when we need it. This approach is cleaner, but the problem with it is that we don’t currently have access to the guesses via the modal component, and passing them just for this feature seems like a major modification.

So, I went with the first approach. In the useWordle.js hook, similar to guesses state, I created squearesStrings state with a very similar logic: Based on the previous squares-strings array, create a new array and add the new guess.

Translating the guess into squares string is pretty straightforward, so we’ll move on and focus on the juicy parts instead 😀

Now that we have an array of the squares representing the guesses, we’ll pass it to the modal and stringify it there. At this point, we will create the resultString variable, which includes the rest of the information that is shared in the message: The Game name, the game number (which we need to pass as a parameter as well), and the number of turns it took.

This worked just fine, so what made me change to the second approach? The answer to that depends on future functionality, so we’ll revisit this one later.

2. “Copy to Clipboard” Functionality

This is a cool feature that it’s implementation is easier than I thought!

The w3schools tutorial is very clear, the only modification we need is to copy our resultString variable and not a text box like they are showing in their demo. Our line of code will be: navigator.clipboard.writeText(resultString);

Our code will look like this:

And in index.css:

I usually don’t share CSS code, because styling is an individual taste. However, this time it contains functionality. For example:

  • v.tooltip .tooltiptext::after {content: ""; ...} will initialize the text after the copy, so if the user clicks the button twice, they won’t get double the content.
  • The visibility property will ensure the tooltip appears when the share button is clicked and disappears when the mouse is not hovering above the button anymore.
Copy to clipboard functionality

A Different View Depends on The User’s Game State

In the previous blog post, we created the grey “Welcome” landing page. Have you ever tried going to the Wordle website after you already played that day in that browser? If you do so, you will see a “Hey, great job today!” page.

While working on this project, I visited the Wordle website a lot. I had to see what’s the scope of every feature and test how it responds to edge cases. It was by accident that I discovered there is a third landing page — the “you’re mid-game, come continue” page!

The different view is not only on the landing page. When the user clicks the button, they see their previous guesses on the grid and the colored keys on the keyboard.


landing page options
A quick recap:

Our game is a single-page application.

“A single-page application is a website that interacts with the user by dynamically rewriting the current web page with new data from the web server, instead of the default method of a web browser loading entire new pages.” Wikipedia

In our case, we don’t use a server-side, but you get the idea. This means that depending on booleans, we show and hide different components: We start with showWordle=false , which makes the user see a grey landing page with two buttons –  Play and How to Play. When they click “Play”, we set the Wordle boolean (showWordle) to true (line 9 below). This makes the Wordle component (the page with the grid and the keyboard) show up and the landing page hides (lines 6,15 below).

When the user clicks “How to Play”, they will see the instructions modal above the Wordle component. This means that you can see the grid page in the modal’s background and closing the modal will reveal the page fully. To achieve that, we need another boolean (line 3 below) to indicate the instructions modal should show as well:

Whether the wordle boolean or the instructions boolean are true (depending on which button the user clicked)— we show the wordle component (line 16 above).

Recap is over, now we can start!


The Post-Game Welcome Page

If we look at the welcome component, the only difference between the two grey pages is the title, description, and button(s).

The indication of which page to show depends on whether the user played today, which is a boolean! We’ll call our boolean gameWasPlayedToday and for now — set it to false (line 3 below). We use this boolean to wrap our existing landing page code (line 8) and the post-game landing page code (lines 14–20).

The post-game welcome page has one button — see stats. Similar to showing the instructions modal, we need to create a boolean and use it for the logic:

  • On line 2 we initialize showProgress to false.
  • On line 17 we set it to true on the button’s onClick function.
  • On lines 6,25 we add this boolean to the page’s conditions.
  • On line 29 we pass the boolean to the Wordle component, which is the one showing/hiding the modal.

Now, all that is left here is to set gameWasPlayedToday correctly (line 3). How do we know if the user played today? We have local storage for statistics  and guess distribution— could that be useful? Yes and no.

The data we save in statistics do not currently indicate if the user played today. We can modify this object to save more information, but I think it’s already big enough, and prefer creating a new local storage object: localStorage.wordlestruckLastGamePlayed.

Creating a new local storage object

There are five things we need to know about the last game played:

  • gameNum will help us know if the last game played was today.
  • turn, isCorrect will be used to color the correct guess distribution bar in green.
  • guesses, usedKeys will be used to populate the grid and the keyboard.

Where does our code determine when a game is over? In the Wordle component’s useEffect! That’s where we will set our new local storage object:

Now we can go back to the landing page component and set gameWasPlayedToday dynamically: We start by setting lastGame to our new localStorage variable (or undefined if there isn’t any). Then we determine if the user played today by comparing the saved game index with today’s game index.

Note that there are two ways to populate gameWasPlayedToday, and you’ll see both in my code. When it’s only one variable dependent on whether last game is defined, I use the inline if, and add lastGame && ...:

var gameWasPlayedToday = lastGame && lastGame.gameNum == index ? true : false;

If there are several variables, I prefer initializing them first and then modifying them in an if block:

var gameWasPlayedToday = false;
If (localStorage.wonderstruckLastGamePlayed){
  let lastGame = JSON.parse(localStorage.wonderstruckLastGamePlayed)
  gameWasPlayedToday = lastGame.gameNum == index ? true : false;

So, we used the stored game index, and we have 4 more variables to utilize: turn, isCorrect, guesses, and usedKeys. We’ll apply the turn and isCorrect variables to update the statistics component.

The post-game landing page has a “see stats” button that opens the progress modal (which is calling the statistics component).

end of game landing page
Post-game functionality. (Don’t worry, I’ll shuffle the solutions before launching the game 😉 )

Up until this point, the turn and isCorrect variables were passed through the component’s parameters, ‘in real time’ when the game was over. We will now pull that data from the local storage variable:

We’ll start with an if condition to check if there is stored data for a previous game played, and initialize the variables for a case there isn’t (this is the “second way to initialize variables based on lastGame” that I mentioned earlier :D).

Did you notice something extra on line 12?

Even with the new landing page, we only reach the progress modal when the game is over. So, why did I add gameWasPlayedToday && ... to the condition for coloring barColor? This is not part of this feature, but part of a new one that I will give you the pleasure of adding yourselves: The header buttons.

The original Wordle has buttons in the main page’s header, that give you access to the instructions and the statistics modals at any time. This means the user can start a new game, and decide they want to check their statistics before the game is over.

In that case, we want all the bars to be grey (and not green), and in my game — I also hide the solution word. In addition, I chose to hide the share button, but this is a personal preference.

progress modal for when todays game wasn't played
The Progress modal before the game is over: All the bars are grey and there is no share button

We already have the functionality to show those modals thanks to the various landing pages, so all that is left to do is create two buttons and setShowProgressModal(true)/ setShowInstructionsModal(true) on click!

Showing guesses and used keys

We made nice progress, but we still didn’t cover all the functionality required for the post-game landing page.

When the user clicks the See Stats button, it will open the progress modal, which we took care of. When the user closes that progress modal, they should see all their past guesses on the grid, but we don’t currently have that information. So… we’ll add it now!

We already have guesses and usedKeys saved in localStorage.wordlestruckLastGamePlayed object, so all that is left to do is add a view option so that if the user already played today, we’ll populate the data with the stored object.

  • On lines 3–10 below, we populate gameWasPlayedToday, gameData.
  • On lines 23–31 is the good old Wordle main page — the grid and keypad components, wrapped with the condition of !gameWasPlayedToday && ....
  • On lines 14–22, we also have the grid and keypad components but this time we populate the properties guesses, turn, and usedKeys with the data from the stored object. And of course, we wrap this block with gameWasPlayedToday && ...

Huston, we have a problem

Another issue that comes up now is that our new shiny share button will be accessible but without a squares-string! This is because we create the squares-string in the useWordle.js hook, but we don’t interact with this part when opening the progress modal directly from the landing page.

I know, we are jumping back and forth, and it must be confusing! No need to scroll up, I’ll give you all the context you need: To let our users show off their game skills, we created squares string representing the game guesses (without exposing the word!).

wordlestruck share
Please don’t take away my Swiftie badge

We talked about two places we can build that string and chose to go with the useWordle.js hook, right after we add a new guess to the guesses array. This is because we dismissed the second option, which is within the share button’s on-click function. We didn’t have access to the guesses via the progress modal component, and passing them just for this feature was too much hustle for the same result.

Well, guess what? We have access to the guesses array from wherever we want, thanks to the localStorage.wordlestruckLastGamePlayed object! Let’s modify the code to create the share-string in the progress modal component.

A few things to note:
  • The guesses array in the useWordle.js hook is initialized to be of size 6 (because there are max 6 guesses), and the guesses are added every turn. This means we can’t simply iterate over the guesses array (guesses.forEach((formattedGuess) => { ...), we need to ensure we iterate only over the populated cells (if(formattedGuess) { ...).
  • Now we can initialize resultString directly with the prefix, instead of adding it after the squaresString creation.
  • At this point, you can remove any code related to the previous version of squaresString we added in useWordle.js, Wordle.js and ProgressModal.js.

A well-deserved meme break:

wordle meme

The Mid-Game Welcome Page

If the user loads the page after entering guesses but before finishing the game, they will see the mid-game welcome page.

welcome back landing page

This page has one button — Continue. When the user clicks it, they see the main page with the guesses they tried so far and the keys they used. To achieve that, we can’t save the game data only when the game ends — we need to save the relevant information after every round ( = after every guess the user entered)! We’ve seen usage of local storage before, but this one is different!

You can either trust me or try yourselves (I encourage both approaches 😉 ), but updating local storage directly won’t work. This is because the Wordle.js component renders a lot more times than you might think —in fact, we trigger page rendering at every key press (due to the handlekey listener). For that reason, if we were to try to continue the game, the typing would cause the data of previous guesses to initialize to an empty array.

The right approach is to add to useWordle.js hook a new state variable: storageData. We will set this variable after every guess entered, and then in Wordle.js we will update local storage with it. That way, in those handlekey renders, the data we set in the useWordle.js hook stays as it was. It only updates on the submission of valid guesses, when the code reaches return {turn, currentGuess, guesses, ...}.

Let’s start coding!

We will begin by initializing variables for each data needed in the hook: initialX, when X is game number, turn, etc. The value will be pulled from gameWasPlayedToday or restarted, if there isn’t any data saved (lines 3–8 below). Then, we will create the new variable storageData and initialize it with those variables (lines 10–17).

Please note that to continue the game at any point, we need to update our localStorage.wordlestruckLastGamePlayed object to contain two more properties: history and isCorrect. This is because we need those for the useWordle hook functionality.

A quick reminder: The difference between history and guesses is that history is a simple array of strings, while guesses is an array of formatted guesses, where each character in each guess is colored.

All that is left to do is add the setter for storage data to addNewGuess() function. We will set the properties just like we set their original counterparts (lines 11–49 below).

Lastly, we will return storageData to the Wordle component (line 52).

Are you seeing what I am seeing?

This code is a bit painful to read because setStorageData contains all the previous sets’ code: setIsCorrect, setGuesses, setHistory, setTurn and SetUsedKeys.

Code duplication is bad practice because now you have multiple places to update code and you might not remember all of them. How about modifying useWordle.js to have only setStorageData? We will do that later, once storageData is up and running, and the application is using it exclusively. We still need to use the data we collected!

In Wordle.js , just above the end of the useEffect scope, we update the local storage with storageData:
localStorage.wordlestruckLastGamePlayed = JSON.stringify(storageData);

Lastly, we need to put the correct boolean to show the mid-game landing page.

A quick reminder:

If the game hasn’t started yet — we want to show the “welcome” landing page. If the game started but is still in progress, we want to show the “welcome back” page, and if the user finished today’s game, we will show the “good job!” page:

We already set gameWasPlayedToday variable when we worked on the post-game landing page:

lastGame && lastGame.gameNo === gameNo ? true : false;

This now needs to change. We modified the code to save state after every turn, therefore the game index in lastGame  indicates this game has started, not necessarily that it’s done.

If gameWasStartedToday is the new gameWasPlayedToday, how do we gather the value for gameWasPlayedToday? No need to save additional variables in local storage, we can just calculate it from the existing stored data:

We know the game started today if the last game’s index is today’s game index. To check if the game was done — we use the same condition as in the Wordle.js component — if isCorrect is true or if the user played their 6th guess.

In case you wondered, I am saving the turn value in a local variable because I need it for the work-in-progress landing page — we want to show the user how many tries they already made.

Back to the Progress Modal!

In the previous blog post, we created the progress modal: It called the statistics component, which shows the user data of games played and % of wins, and below that their guess distribution. This is where we stopped because we needed today’s context to take the modal to the finish line!

progress modal

Bug Fix — Update Statistics Only Once

With today’s changes, we have unlimited access to the Wordle component, and more specifically – access to the end-of-game condition: isCorrect===true || turn>5:

We wrote this code in the previous blog post, go check it if you need a reminder!

This code has become problematic now because every time we go from the “you-already-played-today” landing page to the grid and keyboard page, the if statement (line 5 above) is correct, and we update the statistics (again) on lines 14–15.

Therefore, we need to make sure we update the statistics data only once per game. We can do that by wrapping it in another if condition. That, together with the existing if condition (“if the game is finished”), creates a sweet spot that happens only once in the daily game’s life cycle: The state where local storage’s isCorrect or turn>5 is changing value for the first time!

I created two variables: player_just_won and player_just_lost. Let me remind you we are updating the local storage after every turn. Now, I know the player has just won if isCorrect is true but JSON.parse(localStorage.wordlestruckLastGamePlayed).isCorrect is still false. Same for when the player just lost the game: turn>5 but also JSON.parse(localStorage.wordlestruckLastGamePlayed).turn===5.

It’s also very important to move out the initializing code (lines 7–10 above) to be before the useEffect hook. If you don’t move it, you will encounter a bug later, when you add the statistics button to the header. The bug occurs when the user enters the game for the first time and clicks the statistics button: It will be undefined! Logically, we want localStorage.statistics to be defined when we reach the progress modal. So we’ll define it first thing in the Wordle component.

Our code will now look something like this:

Feature — Adding Streaks Data

To give the progress modal the final touch, we can collect the data for the current strike and the maximum strike.

We’ll start by adding those values to our statistics object, along with currentStreakLastIndex — this is needed to know if the strike continues or not. This means the array of statistics will sit deeper in the object— localStorage.wordlestruckStatistics.stats. We will initialize all the new properties with zero.

Updating the values can be tricky: It’s very important to update them in the correct order! Let’s start with the easier one: maxStreaks is updated only if currentStreak is bigger. we need to check currentStreak‘s value only after we update it today.

currentStreak will be increase only if today’s game continues the streak, which means if today’s game index is currentStreakLastIndex + 1. Otherwise, it will initialize to be 1. This means we should check currentStreakLastIndex‘s value before we update it today.

Lastly, currentStreakLastIndex‘s value should be updated to be today’s game index. Remember that we reach this code only when the game is over, so this game is the current streak’s last index.

Let’s see it in the code:

Important note! The original game is counting streaks of winning, not streaks of playing. I decided to count the streak of playing the game, regardless if the user managed to guess the correct answer or not. If you wish to count streaks for winning, make sure to check if isCorrect is true before updating the new three variables (currentStreak, maxStreak and currentStreakLastIndex).

If you already published your website and sent it to your friends (I’ve mentioned Netlify as a good platform to host your website for free, in the previous blog post), then you should update the name wordlestruckStatistics to something else. Otherwise, you are referencing in your code the developed version of the object (wordlestruckStatistics.stats for example), but your users have the previous version saved in their local storage, so running wordlestruckStatistics.stats will break the application. When you rename that variable, the code will enter the initializing block (if(!wordlestruckStatistics){…}), and you will save yourself a bug.

Bonus Feature — Shaky Rows!

There is one feature from the original game that I’ve decided to not recreate — the “not a real word” feature. When that happens, the row shakes, and a black tooltip appears, notifying you that this word doesn’t exist.

I am still wondering “How did they create a data set of all possible 5-letter words?”. Once you have this data set, it’s pretty easy to notify the user when they have invented a word.

In my game, as you will see in the next blog post, I have a small data set — less than 200. I can check if a guess is within this array, but I don’t think it fits that game, so I decided to not implement it.

With that said, I did want to shake my rows! This is the first turn I took from the original game — I am shaking the current guess row when the user is trying to enter a guess they already made today! In the original game, they let you waste a guess on a duplicate word — so watch out for that!

Well, how do we shake the row?

shaky meme
Shake shake shake

For this, we will need CSS animation. This is not the first time we have used animations in the project. We bounce tiles when the user types in characters, and flip tiles to reveal the characters’ colors! Let’s start by creating the shaking animation in the CSS:

All animations have the same structure: @keyframes <NAME> and division to the percentage of progress. In our case, we want back-and-forth of left and right (lines 2–11 below). Then, we create a new class that will use it (lines 13–15):

Now we need to give the appropriate row div this class name. Which is the appropriate row? The shaking will be of the row that is currently in typing. So, we’ll modify Row.js:

In the code above, we pass a property of shaky to the row component (line 1). The current row will shake (line 8 above) only if the shaky bool is set to true (line 2). Where do we set that value? At the same place we “find out” the user already tried this word — in useWordle hook!

The fun part is that this code was already in the base code, so no extra work was needed from us, just to add the setShaky() function in the right places:

We set shaky to true in the block where we check if the history includes this word (line 8). We also need to set shaky back to false so it will stop shaking after the first character deletion (line 15).

Lastly, as line number 2 implies, we need to add the shaky variable to Wordle.js:
const [shaky, setShaky] = useState(false)

Note that this time we pass the setting function, not the variable:

const useWordle = (…, setShaky)

Look at it shake! Lastly, we need to add a tooltip: We should notify the user why the guess didn’t enter. We already have a tooltip in the code so this will be easy for you to implement by yourselves.

With this done, we successfully finished our list of features!

shaky row
Shaky row in action!

Final Touches

Now that we have a cool game, there are a few steps to make it even cooler!

Refactoring the code to use only storageData:

Remember earlier, when we noticed a disgusting amount of duplicate code in useWordle.js? We will fix it now! Start cautiously by going to Wordle.js and adding variables instead of returning them from the useWordle hook and see if anything breaks.

Once that looks ok, go to useWordle.js and remove those variables and their setters. You will need to use
storageData.<var> instead of those variables in some places, but overall this is a simple change.

Phone screen capability:

Make sure the website view is neat from the phone as well. Check both Android and iPhone, because they don’t present the same way. A useful trick I learned in this project is to set font size or div width with this structure: min(9vw,40px);. Pixels are fixed, but vw is viewport width, which works like a percentage. Keeping the pixel option is good because vw gets too big on the laptop view, and this sizing structure keeps it proportional.


The whole goal of this project is to wink at the original Wordle. What better way to do so, than gather similar fonts as well? My search showed the original Wordle uses a special New York Times font (that I assume is out of reach), so I used Abhaya Libre and AbrilFatface, and I am pleased with the result 🙂

More Refactoring:

At first glance, yes — why bother if the user doesn’t see the code? But, refactoring has two main advantages: First, optimizing the code and using better practices, and names. This will minimize things that could cause you to make oopsies. Second, it will sharpen your React skills. You’ll learn additional (better) ways to produce the same output.

The Only Feature I Couldn’t Figure Out:

In the original game, when the user already played today and clicks the See stats button from the landing page, there is a small delay before showing the modal. This delay exposes the tiles beautifully flipping with the previous guesses. The code, as it is now, has flipping tiles animation, but I didn’t manage to delay the modal.
If you decide to work on this feature, please remember: The modal should be delayed only in two situations: When the user is winning, which is when we have the line setTimeout(() => setShowProgressModal(true), 2000), or when the user arrives from the landing page. There should be no delay when the user clicks the header button!

Wow, what a journey! In this blog post, we worked on the share button, set different views based on the user’s game state, and shook some rows!

In the next blog post, we will transfer our game from a boring 5-letter word to anything you want! Mine is Taylor Swift songs 😀

Exciting news to all the swifties out there — my Wordlestruck game will launch at the end of this blog post series!

See you soon!

Blogging is my hobby, so I happily spend time and money on it. If you enjoyed this blog post, putting 1 euro in my tipping jar will let me know :) Thank you for your support!

Blogging is my hobby, so I happily spend time and money on it. If you enjoyed this blog post, putting 1 euro in my tipping jar will let me know :) Thank you for your support!


Subscribe to Newsletter!

Make sure you never miss any blog post!

I ask for your name so I will a bit about you.
If it doesn’t suit you, write anything you want, preferably something funny like “Ash Ketchum” or “Taylor Swift”.

Related articles

Software Engineer

I believe that anyone can learn anything and I’m here to share knowledge.
I write about things that interest me as a software engineer, and I find interest in various subjects :)

Keep in Touch
Subscribe to Newsletter

Make sure you never miss any blog post!