
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.
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.
<!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>*, *::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;
}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)
})
}
Google Chrome has dominated web browsing for over a decade with 71.77% global market share.…
Perplexity just made its AI-powered browser, Comet, completely free for everyone on October 2, 2025.…
You've probably heard about ChatGPT Atlas, OpenAI's new AI-powered browser that launched on October 21,…
Perplexity Comet became free for everyone on October 2, 2025, bringing research-focused AI browsing to…
ChatGPT Atlas launched on October 21, 2025, but it's only available on macOS. If you're…
Two AI browsers just entered the ring in October 2025, and they're both fighting for…