Back

Wordle 45-Minutes Challenge

Building the full Wordle game in React under time pressure, and what happens when you stop overthinking architecture.

20246 min read

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.

TSX
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.

TSX
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.

TSX
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.

TSX
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:

TSX
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.