I gave myself 45 minutes to build Wordle in React. No planning, no design file, just a timer and a blank Next.js project. The idea came from watching Conner Ardman and Clément Mihailescu do a similar challenge, and I wanted to see how I'd do.
For anyone who hasn't played: you get six tries to guess a five-letter word. Green means right letter, right spot. Yellow means right letter, wrong spot. Gray means not in the word.
The recording
I recorded a sped-up version of the whole process. It's not polished but it's real.
Game state
The first thing I needed was a word list. Found one on GitHub, converted it to a JS array with arrayThis, and set up the core state: a random solution, the current guess, and an array of six slots for previous guesses.
const Wordle = () => {
const [solution, setSolution] = useState('')
const [currentGuess, setCurrentGuess] = useState('')
const [isGameOver, setIsGameOver] = useState(false)
const [guesses, setGuesses] = useState<(string | null)[]>(
Array.from({ length: 6 }).fill(null) as (string | null)[]
)
const fetchNewSolution = useCallback(() => {
const wordsLength = WordsList.length
const randomIndex = Math.floor(Math.random() * wordsLength)
setSolution(WordsList[randomIndex])
}, [])
useEffect(() => {
fetchNewSolution()
}, [])
}
export default Wordle
Input handling
No input field. I used onkeydown directly so typing feels like the real Wordle. Enter submits, Backspace deletes, everything else appends to the current guess.
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') return handleGuess()
if (e.key === 'Backspace')
return setCurrentGuess((currentGuess) => currentGuess.slice(0, -1))
return setCurrentGuess((currentGuess) => currentGuess + e.key)
}
Feedback logic
This is where the actual game lives. Compare the guess to the solution, figure out which letters are green/yellow/gray, and decide if the game is over.
const handleGuess = () => {
const currentGuessLowerCase = currentGuess.toLowerCase()
const solutionLowerCase = solution.toLowerCase()
if (currentGuessLowerCase.length !== 5) return
if (guesses.length === 6) return setIsGameOver(true)
const isSolution = solutionLowerCase === currentGuessLowerCase
if (isSolution) {
setIsGameOver(true)
return
}
setGuesses((guesses) => [
...guesses.slice(0, currentGuess.length),
isSolution ? null : currentGuess,
])
}
Rendering
The UI is a grid of guess rows. Each cell checks its letter against the solution and picks a color. Nothing clever here, just the obvious approach.
return (
<div className="h-screen w-screen flex flex-col items-center justify-center gap-8">
<h1 className="text-3xl font-medium">Wordle</h1>
<div className="flex flex-col items-center gap-y-4">
{guesses.map((guess, index) => (
<Guess
guess={currentGuessIndex === index ? currentGuess : guess ?? ''}
solution={solution}
isStillGuessing={currentGuessIndex === index}
key={index}
/>
))}
</div>
<AnimatePresence mode="wait">
{isGameOver && (
<motion.div
className="fixed inset-0 z-50 bg-gray-100/50 flex items-center justify-center"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5, ease: 'easeInOut' }}
>
<GameOver
isWinner={guesses[currentGuessIndex - 1] === solution}
rounds={currentGuessIndex}
solution={solution}
onPlayAgain={onPlayAgain}
/>
</motion.div>
)}
</AnimatePresence>
</div>
)
The Guess component maps each letter to a colored cell:
const Guess = ({ isStillGuessing, guess, solution }: GuessesProps) => {
const arr = Array(5).fill('')
for (let i = 0; i < guess.length; i++) {
arr[i] = guess[i]
}
return (
<div className="grid grid-cols-5 gap-x-2">
{arr.map((char, index) => {
const isColored = !isStillGuessing && char !== ''
const isGreen = isColored && solution.charAt(index) === char
const isYellow = isColored && !isGreen && solution.includes(char)
const isGray = isColored && !isGreen && !isYellow
return (
<div
className={cn(
'w-14 h-14 border border-gray-200 rounded flex items-center justify-center text-2xl',
{
'bg-green-400': isGreen,
'bg-yellow-400': isYellow,
'bg-gray-200': isGray,
}
)}
key={index}
>
{char}
</div>
)
})}
</div>
)
}
What I learned
The interesting thing about time-boxed challenges is they force you to skip the parts you'd normally overthink. I didn't set up a proper project structure, didn't write types for everything, didn't debate state management patterns. I just wrote the code that solves the problem.
Turns out that's usually enough.