Tutorials

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)
  })
}
Furqan

Well. I've been working for the past three years as a web designer and developer. I have successfully created websites for small to medium sized companies as part of my freelance career. During that time I've also completed my bachelor's in Information Technology.

Recent Posts

How can IT Professionals use ChatGPT?

If you're reading this, you must have heard the buzz about ChatGPT and its incredible…

September 2, 2023

ChatGPT in Cybersecurity: The Ultimate Guide

How to Use ChatGPT in Cybersecurity If you're a cybersecurity geek, you've probably heard about…

September 1, 2023

Add Cryptocurrency Price Widget in WordPress Website

Introduction In the dynamic world of cryptocurrencies, staying informed about the latest market trends is…

August 30, 2023

Best Addons for The Events Calendar Elementor Integration

The Events Calendar Widgets for Elementor has become easiest solution for managing events on WordPress…

August 30, 2023

Create Vertical Timeline in Elementor: A Step-by-step Guide

Introduction The "Story Timeline" is a versatile plugin that offers an innovative way to present…

August 30, 2023

TranslatePress Addon for Automate Page Translation in WordPress

Introduction In today's globalized world, catering to diverse audiences is very important. However, the process…

August 30, 2023