Wordle Clone JavaScript – Full Source Code

In this tutorial, you will learn how to create a JavaScript Wordle clone. I’ll also provide the full wordle source code in this article.

Download Files Containing List of Words

Before getting started, download these JavaScript files and place them in the root folder of your project. Basically, both of these files contain a JavaScript array of all the possible words. These files were huge in size, that’s why I’m providing the download link here instead of the source code.


index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="styles.css">
  <script src="targetWords.js" defer></script>
  <script src="dictionary.js" defer></script>
  <script src="script.js" defer></script>
  <title>Wordle Clone JavaScript</title>
</head>
<body>
  <div class="alert-container" data-alert-container></div>
  <div data-guess-grid class="guess-grid">
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
    <div class="tile"></div>
  </div>
  <div data-keyboard class="keyboard">
    <button class="key" data-key="Q">Q</button>
    <button class="key" data-key="W">W</button>
    <button class="key" data-key="E">E</button>
    <button class="key" data-key="R">R</button>
    <button class="key" data-key="T">T</button>
    <button class="key" data-key="Y">Y</button>
    <button class="key" data-key="U">U</button>
    <button class="key" data-key="I">I</button>
    <button class="key" data-key="O">O</button>
    <button class="key" data-key="P">P</button>
    <div class="space"></div>
    <button class="key" data-key="A">A</button>
    <button class="key" data-key="S">S</button>
    <button class="key" data-key="D">D</button>
    <button class="key" data-key="F">F</button>
    <button class="key" data-key="G">G</button>
    <button class="key" data-key="H">H</button>
    <button class="key" data-key="J">J</button>
    <button class="key" data-key="K">K</button>
    <button class="key" data-key="L">L</button>
    <div class="space"></div>
    <button data-enter class="key large">Enter</button>
    <button class="key" data-key="Z">Z</button>
    <button class="key" data-key="X">X</button>
    <button class="key" data-key="C">C</button>
    <button class="key" data-key="V">V</button>
    <button class="key" data-key="B">B</button>
    <button class="key" data-key="N">N</button>
    <button class="key" data-key="M">M</button>
    <button data-delete class="key large">
      <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
        <path fill="var(--color-tone-1)" 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>
    </button>
  </div>
</body>
</html>

styles.css

*, *::after, *::before {
  box-sizing: border-box;
  font-family: Arial;
}

body {
  background-color: hsl(240, 3%, 7%);
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  margin: 0;
  padding: 1em;
  font-size: clamp(.5rem, 2.5vmin, 1.5rem);
}

.keyboard {
  display: grid;
  grid-template-columns: repeat(20, minmax(auto, 1.25em));
  grid-auto-rows: 3em;
  gap: .25em;
  justify-content: center;
}

.key {
  font-size: inherit;
  grid-column: span 2;
  border: none;
  padding: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: hsl(
    var(--hue, 200),
    var(--saturation, 1%),
    calc(var(--lightness-offset, 0%) + var(--lightness, 51%))
  );
  color: white;
  fill: white;
  text-transform: uppercase;
  border-radius: .25em;
  cursor: pointer;
  user-select: none;
}

.key.large {
  grid-column: span 3;
}

.key > svg {
  width: 1.75em;
  height: 1.75em;
}

.key:hover, .key:focus {
  --lightness-offset: 10%;
}

.key.wrong {
  --lightness: 23%;
}

.key.wrong-location {
  --hue: 49;
  --saturation: 51%;
  --lightness: 47%;
}

.key.correct {
  --hue: 115;
  --saturation: 29%;
  --lightness: 43%;
}

.guess-grid {
  display: grid;
  justify-content: center;
  align-content: center;
  flex-grow: 1;
  grid-template-columns: repeat(5, 4em);
  grid-template-rows: repeat(6, 4em);
  gap: .25em;
  margin-bottom: 1em;
}

.tile {
  font-size: 2em;
  color: white;
  border: .05em solid hsl(240, 2%, 23%);
  text-transform: uppercase;
  font-weight: bold;
  display: flex;
  justify-content: center;
  align-items: center;
  user-select: none;
  transition: transform 250ms linear;
}

.tile[data-state="active"] {
  border-color: hsl(200, 1%, 34%);
}

.tile[data-state="wrong"] {
  border: none;
  background-color: hsl(240, 2%, 23%);
}

.tile[data-state="wrong-location"] {
  border: none;
  background-color: hsl(49, 51%, 47%);
}

.tile[data-state="correct"] {
  border: none;
  background-color: hsl(115, 29%, 43%);
}

.tile.shake {
  animation: shake 250ms ease-in-out;
}

.tile.dance {
  animation: dance 500ms ease-in-out;
}

.tile.flip {
  transform: rotateX(90deg);
}

@keyframes shake {
  10% {
    transform: translateX(-5%);
  }

  30% {
    transform: translateX(5%);
  }

  50% {
    transform: translateX(-7.5%);
  }

  70% {
    transform: translateX(7.5%);
  }

  90% {
    transform: translateX(-5%);
  }

  100% {
    transform: translateX(0);
  }
}

@keyframes dance {
  20% {
    transform: translateY(-50%);
  }  

  40% {
    transform: translateY(5%);
  }  

  60% {
    transform: translateY(-25%);
  }  

  80% {
    transform: translateY(2.5%);
  }  

  90% {
    transform: translateY(-5%);
  }  

  100% {
    transform: translateY(0);
  }
}

.alert-container {
  position: fixed;
  top: 10vh;
  left: 50vw;
  transform: translateX(-50%);
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.alert {
  pointer-events: none;
  background-color: hsl(204, 7%, 85%);
  padding: .75em;
  border-radius: .25em;
  opacity: 1;
  transition: opacity 500ms ease-in-out;
  margin-bottom: .5em;
}

.alert:last-child {
  margin-bottom: 0;
}

.alert.hide {
  opacity: 0;
}

script.js

const WORD_LENGTH = 5
const FLIP_ANIMATION_DURATION = 500
const DANCE_ANIMATION_DURATION = 500
const keyboard = document.querySelector("[data-keyboard]")
const alertContainer = document.querySelector("[data-alert-container]")
const guessGrid = document.querySelector("[data-guess-grid]")
const offsetFromDate = new Date(2022, 0, 1)
const msOffset = Date.now() - offsetFromDate
const dayOffset = msOffset / 1000 / 60 / 60 / 24
const targetWord = targetWords[Math.floor(dayOffset)]

startInteraction()

function startInteraction() {
  document.addEventListener("click", handleMouseClick)
  document.addEventListener("keydown", handleKeyPress)
}

function stopInteraction() {
  document.removeEventListener("click", handleMouseClick)
  document.removeEventListener("keydown", handleKeyPress)
}

function handleMouseClick(e) {
  if (e.target.matches("[data-key]")) {
    pressKey(e.target.dataset.key)
    return
  }

  if (e.target.matches("[data-enter]")) {
    submitGuess()
    return
  }

  if (e.target.matches("[data-delete]")) {
    deleteKey()
    return
  }
}

function handleKeyPress(e) {
  if (e.key === "Enter") {
    submitGuess()
    return
  }

  if (e.key === "Backspace" || e.key === "Delete") {
    deleteKey()
    return
  }

  if (e.key.match(/^[a-z]$/)) {
    pressKey(e.key)
    return
  }
}

function pressKey(key) {
  const activeTiles = getActiveTiles()
  if (activeTiles.length >= WORD_LENGTH) return
  const nextTile = guessGrid.querySelector(":not([data-letter])")
  nextTile.dataset.letter = key.toLowerCase()
  nextTile.textContent = key
  nextTile.dataset.state = "active"
}

function deleteKey() {
  const activeTiles = getActiveTiles()
  const lastTile = activeTiles[activeTiles.length - 1]
  if (lastTile == null) return
  lastTile.textContent = ""
  delete lastTile.dataset.state
  delete lastTile.dataset.letter
}

function submitGuess() {
  const activeTiles = [...getActiveTiles()]
  if (activeTiles.length !== WORD_LENGTH) {
    showAlert("Not enough letters")
    shakeTiles(activeTiles)
    return
  }

  const guess = activeTiles.reduce((word, tile) => {
    return word + tile.dataset.letter
  }, "")

  if (!dictionary.includes(guess)) {
    showAlert("Not in word list")
    shakeTiles(activeTiles)
    return
  }

  stopInteraction()
  activeTiles.forEach((...params) => flipTile(...params, guess))
}

function flipTile(tile, index, array, guess) {
  const letter = tile.dataset.letter
  const key = keyboard.querySelector(`[data-key="${letter}"i]`)
  setTimeout(() => {
    tile.classList.add("flip")
  }, (index * FLIP_ANIMATION_DURATION) / 2)

  tile.addEventListener(
    "transitionend",
    () => {
      tile.classList.remove("flip")
      if (targetWord[index] === letter) {
        tile.dataset.state = "correct"
        key.classList.add("correct")
      } else if (targetWord.includes(letter)) {
        tile.dataset.state = "wrong-location"
        key.classList.add("wrong-location")
      } else {
        tile.dataset.state = "wrong"
        key.classList.add("wrong")
      }

      if (index === array.length - 1) {
        tile.addEventListener(
          "transitionend",
          () => {
            startInteraction()
            checkWinLose(guess, array)
          },
          { once: true }
        )
      }
    },
    { once: true }
  )
}

function getActiveTiles() {
  return guessGrid.querySelectorAll('[data-state="active"]')
}

function showAlert(message, duration = 1000) {
  const alert = document.createElement("div")
  alert.textContent = message
  alert.classList.add("alert")
  alertContainer.prepend(alert)
  if (duration == null) return

  setTimeout(() => {
    alert.classList.add("hide")
    alert.addEventListener("transitionend", () => {
      alert.remove()
    })
  }, duration)
}

function shakeTiles(tiles) {
  tiles.forEach(tile => {
    tile.classList.add("shake")
    tile.addEventListener(
      "animationend",
      () => {
        tile.classList.remove("shake")
      },
      { once: true }
    )
  })
}

function checkWinLose(guess, tiles) {
  if (guess === targetWord) {
    showAlert("You Win", 5000)
    danceTiles(tiles)
    stopInteraction()
    return
  }

  const remainingTiles = guessGrid.querySelectorAll(":not([data-letter])")
  if (remainingTiles.length === 0) {
    showAlert(targetWord.toUpperCase(), null)
    stopInteraction()
  }
}

function danceTiles(tiles) {
  tiles.forEach((tile, index) => {
    setTimeout(() => {
      tile.classList.add("dance")
      tile.addEventListener(
        "animationend",
        () => {
          tile.classList.remove("dance")
        },
        { once: true }
      )
    }, (index * DANCE_ANIMATION_DURATION) / 5)
  })
}

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.