Learn how to create a modern, responsive Wordle clone with this step-by-step tutorial. Get free source code for a fully functional word puzzle game using HTML, CSS, and JavaScript
Table of Contents
Wordle took the world by storm with its simple yet addictive gameplay. If you are a developer looking to sharpen your skills in CSS Grid, Flexbox, and Async JavaScript, building a Wordle clone is the perfect project. In this guide, we provide a "Free Code Snippet" of a professional-grade Wordle clone, featuring smooth animations, a statistics tracker, and a fully responsive design.
Prerequisites
Before we dive in, ensure you have:
- A basic understanding of HTML tags and structure.
- Familiarity with CSS styling and animations.
- Fundamental knowledge of JavaScript (Arrays, DOM manipulation, and LocalStorage).
- A code editor like VS Code and a modern web browser.
Source Code
Step 1 (HTML Code):
The structure of our Wordle game is divided into three main sections: the Header, the Game Board, and the Interactive Keyboard. We also include Modals for instructions and statistics.
Step 2 (CSS Code):
To make the game eye-catching and modern, we use the Inter and Poppins font families. We utilize CSS Grid to create a perfectly aligned 5x6 board and a responsive keyboard that works on mobile devices.
:root {
--bg: #ffffff;
--text: #121213;
--border: #e5e7eb;
--absent: #71717a;
--present: #eab308;
--correct: #22c55e;
--key-bg: #e5e7eb;
--key-text: #1a1a1b;
--tile-text: #1a1a1b;
--tile-border: #d1d5db;
--header-border: #f3f4f6;
--accent: #3b82f6;
--shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Inter', sans-serif;
-webkit-tap-highlight-color: transparent;
}
h1, h2, h3 {
font-family: 'Poppins', sans-serif;
}
body {
background-color: var(--bg);
color: var(--text);
display: flex;
flex-direction: column;
align-items: center;
height: 100vh;
/* overflow: hidden; */
}
header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 500px;
padding: 12px 20px;
border-bottom: 1px solid var(--header-border);
flex-shrink: 0;
}
header h1 {
font-size: 1.6rem;
font-weight: 700;
letter-spacing: -0.5px;
color: var(--text);
background: linear-gradient(135deg, #121213 0%, #3b82f6 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.icon-btn {
background: #f9fafb;
border: 1px solid var(--border);
color: var(--text);
font-size: 1.1rem;
cursor: pointer;
padding: 8px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
box-shadow: var(--shadow);
}
.icon-btn:active {
transform: scale(0.95);
}
#game-container {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 500px;
padding: 15px;
}
#board-container {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
#board {
display: grid;
grid-template-rows: repeat(6, 1fr);
gap: 8px;
}
.row {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
.tile {
width: clamp(48px, 13vw, 62px);
height: clamp(48px, 13vw, 62px);
border: 2px solid var(--tile-border);
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
font-size: clamp(1.4rem, 6vw, 2rem);
font-weight: 700;
text-transform: uppercase;
user-select: none;
transition: transform 0.1s linear, background-color 0.4s ease, border-color 0.4s ease;
}
.tile[data-state="active"] {
border-color: #121213;
transform: scale(1.05);
}
.tile.pop {
transform: scale(1.15);
}
.tile.flip {
transform: rotateX(90deg);
}
.tile[data-state="absent"] {
background-color: var(--absent);
border-color: var(--absent);
color: white;
}
.tile[data-state="present"] {
background-color: var(--present);
border-color: var(--present);
color: white;
}
.tile[data-state="correct"] {
background-color: var(--correct);
border-color: var(--correct);
color: white;
}
#keyboard {
display: grid;
grid-template-rows: repeat(3, 1fr);
gap: 8px;
width: 100%;
padding: 15px 0;
flex-shrink: 0;
}
.keyboard-row {
display: flex;
justify-content: center;
gap: 6px;
}
.key {
background-color: var(--key-bg);
color: var(--key-text);
border: none;
border-radius: 6px;
height: clamp(48px, 7vh, 60px);
flex: 1;
display: flex;
justify-content: center;
align-items: center;
font-weight: 600;
font-size: clamp(0.75rem, 3.5vw, 1rem);
cursor: pointer;
user-select: none;
text-transform: uppercase;
transition: background-color 0.2s, transform 0.1s;
}
.key:active {
transform: translateY(2px);
filter: brightness(0.9);
}
.key.large {
flex: 1.5;
font-size: 0.8rem;
}
.key[data-state="absent"] {
background-color: #3f3f46;
color: white;
}
.key[data-state="present"] {
background-color: var(--present);
color: white;
}
.key[data-state="correct"] {
background-color: var(--correct);
color: white;
}
#message-container {
position: fixed;
top: 12%;
left: 50%;
transform: translateX(-50%);
z-index: 2000;
pointer-events: none;
width: 90%;
max-width: 350px;
text-align: center;
}
.toast {
background-color: #121213;
color: white;
padding: 14px 24px;
border-radius: 12px;
margin-bottom: 10px;
font-weight: 600;
font-size: 0.95rem;
opacity: 0;
transform: translateY(-20px);
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 10px 15px -3px rgba(0,0,0,0.3);
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
/* Modals */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.4);
backdrop-filter: blur(4px);
z-index: 1000;
justify-content: center;
align-items: center;
padding: 20px;
}
.modal-content {
background-color: var(--bg);
color: var(--text);
padding: 32px;
border-radius: 24px;
max-width: 420px;
width: 100%;
text-align: center;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
position: relative;
}
.modal h2 {
margin-bottom: 20px;
font-size: 1.5rem;
letter-spacing: -0.5px;
}
.stats-grid {
display: flex;
justify-content: space-around;
margin-bottom: 25px;
background: #f9fafb;
padding: 20px;
border-radius: 16px;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-val { font-size: 1.8rem; font-weight: 700; color: #121213; }
.stat-label { font-size: 0.7rem; text-transform: uppercase; font-weight: 600; color: #71717a; }
.btn-container {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
}
.btn-primary {
background-color: var(--correct);
color: white;
border: none;
padding: 14px 24px;
border-radius: 12px;
cursor: pointer;
font-weight: 700;
font-size: 1rem;
width: 100%;
transition: all 0.2s ease;
box-shadow: 0 4px 0px #16a34a;
}
.btn-primary:active {
transform: translateY(2px);
box-shadow: 0 2px 0px #16a34a;
}
.btn-secondary {
background-color: #f3f4f6;
color: #4b5563;
border: none;
padding: 12px;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
font-size: 0.9rem;
}
.btn-danger {
background-color: #fee2e2;
color: #dc2626;
border: none;
padding: 8px;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 0.75rem;
margin-top: 15px;
opacity: 0.8;
}
#game-over-msg {
font-size: 1.1rem;
margin-bottom: 20px;
padding: 15px;
border-radius: 16px;
background: #f1f5f9;
font-weight: 500;
line-height: 1.5;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
20%, 60% { transform: translateX(-6px); }
40%, 80% { transform: translateX(6px); }
} Step 3 (JavaScript Code):
The JavaScript is the "brain" of the game. It handles input, validates guesses against the target word, manages animations, and saves player statistics to localStorage.
const WORD_LENGTH = 5;
const MAX_GUESSES = 6;
const WORDS = [
'APPLE',
'BEACH',
'BRAIN',
'BREAD',
'BRUSH',
'CHAIR',
'CHEST',
'CHORD',
'CLICK',
'CLOCK',
'CLOUD',
'DANCE',
'DIARY',
'DRINK',
'DRIVE',
'EARTH',
'FEAST',
'FIELD',
'FRUIT',
'GLASS',
'GRAPE',
'GREEN',
'GROUND',
'GUARD',
'HEART',
'HOUSE',
'JUICE',
'LIGHT',
'LEMON',
'MUSIC',
'NIGHT',
'OCEAN',
'PIANO',
'PILOT',
'PLANE',
'PHONE',
'PIZZA',
'PLANT',
'RADIO',
'RIVER',
'ROBOT',
'SHIRT',
'SHOES',
'SMILE',
'SNAKE',
'SPACE',
'SPOON',
'STORM',
'TABLE',
'TIGER',
'TOAST',
'TOUCH',
'TRAIN',
'TRUCK',
'VOICE',
'WATER',
'WATCH',
'WHALE',
'WORLD',
'WRITE',
'YOUTH',
'ZEBRA',
];
let targetWord = '';
let currentGuess = '';
let guesses = [];
let gameOver = false;
let isAnimating = false; // Flag to prevent input during animations
let stats = JSON.parse(localStorage.getItem('wordle-stats')) || {
played: 0,
wins: 0,
streak: 0,
maxStreak: 0,
};
const boardElement = document.getElementById('board');
const keyboardElement = document.getElementById('keyboard');
const messageContainer = document.getElementById('message-container');
function init() {
targetWord = WORDS[Math.floor(Math.random() * WORDS.length)];
createBoard();
createKeyboard();
setupEventListeners();
}
function createBoard() {
boardElement.innerHTML = '';
for (let i = 0; i < MAX_GUESSES; i++) {
const row = document.createElement('div');
row.className = 'row';
for (let j = 0; j < WORD_LENGTH; j++) {
const tile = document.createElement('div');
tile.className = 'tile';
tile.id = `tile-${i}-${j}`;
row.appendChild(tile);
}
boardElement.appendChild(row);
}
}
function createKeyboard() {
const layout = [
['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],
['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'],
['ENTER', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', 'DEL'],
];
keyboardElement.innerHTML = '';
layout.forEach((rowKeys) => {
const row = document.createElement('div');
row.className = 'keyboard-row';
rowKeys.forEach((key) => {
const btn = document.createElement('button');
btn.className =
'key' + (key === 'ENTER' || key === 'DEL' ? ' large' : '');
btn.textContent = key;
btn.id = `key-${key}`;
btn.onclick = (e) => {
e.preventDefault();
handleInput(key);
};
row.appendChild(btn);
});
keyboardElement.appendChild(row);
});
}
function handleInput(key) {
if (gameOver || isAnimating) return;
if (key === 'ENTER') {
submitGuess();
} else if (key === 'DEL' || key === 'Backspace') {
deleteLetter();
} else if (/^[A-Z]$/i.test(key) && currentGuess.length < WORD_LENGTH) {
addLetter(key.toUpperCase());
}
}
function addLetter(letter) {
if (currentGuess.length >= WORD_LENGTH) return;
const tile = document.getElementById(
`tile-${guesses.length}-${currentGuess.length}`
);
if (tile) {
currentGuess += letter;
tile.textContent = letter;
tile.setAttribute('data-state', 'active');
tile.classList.add('pop');
setTimeout(() => tile.classList.remove('pop'), 100);
}
}
function deleteLetter() {
if (currentGuess.length === 0) return;
const tile = document.getElementById(
`tile-${guesses.length}-${currentGuess.length - 1}`
);
if (tile) {
tile.textContent = '';
tile.removeAttribute('data-state');
currentGuess = currentGuess.slice(0, -1);
}
}
async function submitGuess() {
if (currentGuess.length !== WORD_LENGTH) {
showMessage('Not enough letters');
shakeRow(guesses.length);
return;
}
isAnimating = true;
const row = guesses.length;
const result = checkGuess(currentGuess, targetWord);
keyboardElement.style.pointerEvents = 'none';
for (let i = 0; i < WORD_LENGTH; i++) {
const tile = document.getElementById(`tile-${row}-${i}`);
if (tile) {
tile.classList.add('flip');
await new Promise((r) => setTimeout(r, 200));
tile.setAttribute('data-state', result[i]);
tile.classList.remove('flip');
updateKeyColor(currentGuess[i], result[i]);
}
}
keyboardElement.style.pointerEvents = 'auto';
guesses.push(currentGuess);
isAnimating = false;
if (currentGuess === targetWord) {
handleWin();
} else if (guesses.length === MAX_GUESSES) {
handleLoss();
}
currentGuess = '';
}
function checkGuess(guess, target) {
const res = Array(WORD_LENGTH).fill('absent');
const targetArr = target.split('');
const guessArr = guess.split('');
for (let i = 0; i < WORD_LENGTH; i++) {
if (guessArr[i] === targetArr[i]) {
res[i] = 'correct';
targetArr[i] = null;
guessArr[i] = null;
}
}
for (let i = 0; i < WORD_LENGTH; i++) {
if (guessArr[i] && targetArr.includes(guessArr[i])) {
res[i] = 'present';
targetArr[targetArr.indexOf(guessArr[i])] = null;
}
}
return res;
}
function updateKeyColor(letter, state) {
const key = document.getElementById(`key-${letter}`);
if (!key) return;
const currentState = key.getAttribute('data-state');
if (currentState === 'correct') return;
if (currentState === 'present' && state === 'absent') return;
key.setAttribute('data-state', state);
}
function handleWin() {
gameOver = true;
stats.played++;
stats.wins++;
stats.streak++;
stats.maxStreak = Math.max(stats.streak, stats.maxStreak);
saveStats();
const msg = `Splendid! You found <b>${targetWord}</b>.`;
document.getElementById('stats-title').textContent = 'CONGRATS! 🎉';
document.getElementById('game-over-msg').innerHTML = msg;
setTimeout(() => {
showMessage('Winner!');
openModal('stats-modal');
}, 800);
}
function handleLoss() {
gameOver = true;
stats.played++;
stats.streak = 0;
saveStats();
const msg = `Unlucky. The correct word was <b>${targetWord}</b>. Better luck next time!`;
document.getElementById('stats-title').textContent = 'GAME OVER';
document.getElementById('game-over-msg').innerHTML = msg;
setTimeout(() => {
showMessage(targetWord);
openModal('stats-modal');
}, 800);
}
function showMessage(msg) {
const toast = document.createElement('div');
toast.className = 'toast';
toast.innerHTML = msg;
messageContainer.appendChild(toast);
setTimeout(() => toast.classList.add('show'), 10);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 250);
}, 2500);
}
function shakeRow(rowIdx) {
const rows = document.querySelectorAll('.row');
if (rows[rowIdx]) {
rows[rowIdx].style.animation = 'shake 0.5s';
setTimeout(() => (rows[rowIdx].style.animation = ''), 500);
}
}
function openModal(id) {
document.getElementById('stat-played').textContent = stats.played;
document.getElementById('stat-win').textContent = stats.played
? Math.round((stats.wins / stats.played) * 100)
: 0;
document.getElementById('stat-streak').textContent = stats.streak;
document.getElementById('stat-max').textContent = stats.maxStreak;
document.getElementById(id).style.display = 'flex';
}
function closeModal(id) {
document.getElementById(id).style.display = 'none';
}
function saveStats() {
localStorage.setItem('wordle-stats', JSON.stringify(stats));
}
function resetStats() {
if (confirm('Reset your statistics? This cannot be undone.')) {
stats = { played: 0, wins: 0, streak: 0, maxStreak: 0 };
saveStats();
openModal('stats-modal');
showMessage('Progress Reset');
}
}
function resetGame() {
closeModal('stats-modal');
document.getElementById('game-over-msg').innerHTML = '';
document.getElementById('stats-title').textContent = 'STATISTICS';
currentGuess = '';
guesses = [];
gameOver = false;
isAnimating = false;
targetWord = WORDS[Math.floor(Math.random() * WORDS.length)];
createBoard();
createKeyboard();
showMessage('New Word Loaded!');
}
function setupEventListeners() {
window.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey || e.altKey) return;
handleInput(e.key.toUpperCase());
});
document.getElementById('help-btn').onclick = () => openModal('help-modal');
document.getElementById('stats-btn').onclick = () => openModal('stats-modal');
}
window.onload = init;Final Output:
Conclusion:
Creating a Wordle clone is an excellent way to practice modern web development techniques. This version includes advanced features like animation locking to prevent input bugs, responsive scaling for mobile users, and a persistent statistics system.
Feel free to customize the word list, adjust the colors, or add a dark mode to make it your own!
That’s a wrap!
I hope you enjoyed this post. Now, with these examples, you can create your own amazing page.
Did you like it? Let me know in the comments below 🔥 and you can support me by buying me a coffee
And don’t forget to sign up to our email newsletter so you can get useful content like this sent right to your inbox!
Thanks!
Faraz 😊


