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!
- Guess the WORDLE in 6 tries.
- Each guess must be a valid 5 letter word. Hit the enter button to submit.
- After each guess, the color of the tiles will change to show how close your guess was to the word.
π Let's build it!
This project uses:
#react
#styled-components
π¨ Basic Styling and Layout
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.
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;
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;
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>
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>
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>
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;
π§ 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>
);
}
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>
);
}
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);
};
}, []);
...
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>
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);
}, []);
...
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;
}
};
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;
`;
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;
};
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);
}, []);
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;
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!
Top comments (0)