Build a 2-Player Chess Game
Build a fully playable chess game in the browser with JavaScript.
Introduction
30 Second Summary
Chess is the game everyone knows but few people have built from scratch. Imagine opening your browser, clicking a pawn, and watching it glide forward with full rule enforcement happening behind the scenes.
In this project, you will build a 2-player chess game entirely in the browser using HTML, CSS, and JavaScript. Two players share a screen, take turns clicking pieces, and the game handles every rule automatically.
What You'll Build
You open the page, enter two player names, click Start, and a polished chessboard appears. You click a piece, see where it can go, and click to move it. The game announces check, detects checkmate, and declares the winner by name.
By the end of this project, you'll have:
- A fully playable chess game where you click to select a piece and click to move it, with castling, en passant, and pawn promotion all handled for you.
- A Practice mode that highlights every legal move in green so a beginner can learn while playing, and a Normal mode with no hints for a real challenge.
- A start screen where both players type their names and a turn indicator that announces whose move it is throughout the game.
- Secret Mission: A captured pieces display and an undo button that reverses the last full move.
Are there any prerequisites?
None. This guide walks you through installing everything you need, including VS Code and a browser preview tool. No prior game-building experience is required.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters to you. Taking a moment to set your intention keeps you focused as you work through each step.
Set Up the Project
To build an interactive chess game, you need a web page that connects three files: HTML for structure, CSS for styling, and JavaScript for game logic.
In this step, you'll write the HTML skeleton that ties everything together and launch Live Server to preview it in your browser.
In this step, get ready to:
- Add an HTML skeleton to index.html that links your CSS and JavaScript files.
- Launch Live Server and confirm the page loads in your browser.
Add the HTML skeleton
Every web page starts with an HTML skeleton that tells the browser what to load. Your skeleton will link style.css for styling and script.js as an ES Module so you can use modern JavaScript imports later.
- In VS Code, click on index.html in the Explorer sidebar to open it.
- Replace everything in the file with the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chess Game</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Chess Game</h1>
<script type="module" src="script.js"></script>
</body>
</html>
What does this code do?
- <link rel="stylesheet" href="style.css"> connects your stylesheet so any CSS you write will apply to the page.
- <script type="module" src="script.js"> loads your JavaScript as a module, which lets you use import statements to pull in external libraries later.
- The <h1> heading gives you something visible to confirm the page is loading correctly.
- Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
✔️ Awesome, I've got everything!
Great. Double check you've saved your file, then move on to launching Live Server below.
ⓧ I'd like to double check the full code
Here is the complete index.html file. Make sure yours matches exactly.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chess Game</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Chess Game</h1>
<script type="module" src="script.js"></script>
</body>
</html>
Launch Live Server and verify in the browser
Live Server runs a local web server that automatically refreshes your browser whenever you save a file. This means you can see changes instantly as you build the chess game.
- In the VS Code Explorer sidebar, right-click on index.html.
- Select Open with Live Server from the context menu.
Before the page opens, what do you expect to see in the browser?
Your browser opens automatically to http://127.0.0.1:5500. You should see the heading "Chess Game" displayed on the page.
Don't see the page?
- Make sure you right-clicked index.html specifically, not one of the other files or the folder name.
- Check that Live Server is installed by looking for Go Live in the bottom-right corner of the VS Code status bar. If it is missing, return to the Extensions panel and install it.
- If the browser opens but the page is blank, confirm that your index.html is saved (the filename in the tab should not show a dot or circle indicating unsaved changes).
Help me troubleshoot Live Server not loading my page.
Your project skeleton is wired up and Live Server is running. Next up, you'll build the 8x8 chessboard and place all 32 pieces on it.
Build the Chessboard with Pieces
Your environment is set up and Live Server is running in the background. Now it's time to see something real on screen.
Before anything can be interactive, you need the board and pieces visible. This step renders the 8x8 grid using CSS Grid and places professional SVG piece images in their starting positions using a library called chess.js.
In this step, get ready to:
- Build the HTML game container structure and load chess.js.
- Style the board as an 8x8 grid with CSS Grid.
- Render piece images in their starting positions.
Build the HTML structure and load chess.js
The game needs a container to hold the board and a status area for messages. You also need a chess engine to tell you where all the pieces start.
- Replace the entire contents of index.html with this HTML structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chess Game</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="game-container">
<div id="status"></div>
<div id="board"></div>
</div>
<script type="module" src="script.js"></script>
</body>
</html>
What changed in the HTML?
The <h1> heading is gone. In its place, a game-container div wraps two children: a status div for turn messages and a board div where JavaScript will generate the 64 squares.
- Save index.html.
Now you need JavaScript to populate that empty board div with 64 squares. chess.js is a library that knows all the rules of chess and where every piece starts.
- Paste the following code into script.js to import chess.js and generate the board squares:
import { Chess } from 'https://cdn.jsdelivr.net/npm/chess.js@1.4.0/dist/chess.js';
const chess = new Chess();
const boardElement = document.getElementById('board');
const statusElement = document.getElementById('status');
function renderBoard() {
boardElement.innerHTML = '';
const board = chess.board();
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const square = document.createElement('div');
const isLight = (row + col) % 2 === 0;
square.classList.add('square', isLight ? 'light' : 'dark');
const file = 'abcdefgh'[col];
const rank = 8 - row;
const position = file + rank;
square.dataset.position = position;
boardElement.appendChild(square);
}
}
}
renderBoard();
What does this code do?
The import line loads the chess.js library directly from a CDN using ES Modules. No install step or build tools needed.
The renderBoard function reads the board state with chess.board() (a 2D array of piece positions) and creates 64 div elements. Each square gets a light or dark class based on its position, and a data-position attribute (like e2) for later click handling.
- Save script.js.
- Switch to your browser. Right-click anywhere on the page and select Inspect to open DevTools.
- In the Elements tab, expand the #board div. You should see 64 div.square elements with alternating light and dark classes.
Not seeing 64 squares in DevTools?
- Check the browser console (Console tab in DevTools) for errors. If you see a CORS or import error, make sure you are viewing the page through Live Server (http://127.0.0.1:5500) and not opening the file directly.
- Confirm script.js is loaded as a module. The <script> tag in index.html must have type="module".
- help me debug why my chess.js import is failing
Style the board with CSS Grid
The 64 squares exist in the DOM but are invisible without styling. CSS Grid turns them into a proper 8x8 chessboard layout.
- Paste the following into style.css to set up the page layout and container styles:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #1a1a2e;
color: #eee;
}
#game-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
#status {
font-size: 1.2rem;
font-weight: bold;
padding: 8px 16px;
background: #16213e;
border-radius: 6px;
min-height: 40px;
display: flex;
align-items: center;
}
What does this CSS do?
The universal reset (*) removes default browser spacing so everything lines up predictably. The body uses flexbox to center the game on a dark background.
The #game-container stacks the status bar and board vertically. The #status area is styled as a pill-shaped bar that will show turn messages later.
- Add the board and square styles below the existing code in style.css:
#board {
display: grid;
grid-template-columns: repeat(8, 70px);
grid-template-rows: repeat(8, 70px);
border: 3px solid #333;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}
.square {
width: 70px;
height: 70px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
position: relative;
}
.square.light {
background-color: #f0d9b5;
}
.square.dark {
background-color: #b58863;
}
.square img {
width: 60px;
height: 60px;
pointer-events: none;
}
What does this CSS do?
The #board uses display: grid with grid-template-columns: repeat(8, 70px) to arrange the 64 squares into 8 columns of 8 rows. This is the key property that makes a flat list of divs appear as a grid.
The .square.light and .square.dark classes give the classic tan and brown alternating pattern. The .square img rule sizes future piece images and prevents them from stealing click events.
- Save style.css and switch to your browser.
- You should see an 8x8 grid of alternating tan and brown squares centered on a dark background. No pieces yet.
Squares appear in a single column?
- Make sure the #board selector in your CSS has display: grid and grid-template-columns: repeat(8, 70px). Without both properties, the squares stack vertically.
- Confirm the <link> tag in index.html points to style.css (not styles.css or another filename).
- help me fix my CSS Grid chessboard layout
Render pieces as images
The board looks right, but it is empty. You need a mapping from chess.js piece data to actual image URLs so each square can show the correct piece graphic.
- In script.js, find the line that says const chess = new Chess(); and paste the following object directly below it:
const PIECE_IMAGES = {
wK: 'https://upload.wikimedia.org/wikipedia/commons/4/42/Chess_klt45.svg',
wQ: 'https://upload.wikimedia.org/wikipedia/commons/1/15/Chess_qlt45.svg',
wR: 'https://upload.wikimedia.org/wikipedia/commons/7/72/Chess_rlt45.svg',
wB: 'https://upload.wikimedia.org/wikipedia/commons/b/b1/Chess_blt45.svg',
wN: 'https://upload.wikimedia.org/wikipedia/commons/7/70/Chess_nlt45.svg',
wP: 'https://upload.wikimedia.org/wikipedia/commons/4/45/Chess_plt45.svg',
bK: 'https://upload.wikimedia.org/wikipedia/commons/f/f0/Chess_kdt45.svg',
bQ: 'https://upload.wikimedia.org/wikipedia/commons/4/47/Chess_qdt45.svg',
bR: 'https://upload.wikimedia.org/wikipedia/commons/f/ff/Chess_rdt45.svg',
bB: 'https://upload.wikimedia.org/wikipedia/commons/9/98/Chess_bdt45.svg',
bN: 'https://upload.wikimedia.org/wikipedia/commons/e/ef/Chess_ndt45.svg',
bP: 'https://upload.wikimedia.org/wikipedia/commons/c/c7/Chess_pdt45.svg',
};
What are these URLs?
These are SVG chess piece graphics from Wikimedia Commons, created by Cburnett. They are the same professional pieces used on Wikipedia's chess articles. Each key combines a color prefix (w or b) with a piece type (K, Q, R, B, N, P).
Now you need a helper function that converts chess.js piece data into the correct key, and code inside the render loop that creates an <img> element for each occupied square.
- In script.js, find the line function renderBoard() { and paste this helper function directly above it:
function getPieceImageKey(piece) {
// Combine color (w/b) with uppercase type (K/Q/R/B/N/P)
const colorPrefix = piece.color === 'w' ? 'w' : 'b';
const typeChar = piece.type.toUpperCase();
return colorPrefix + typeChar;
}
- Inside the renderBoard function, find the line boardElement.appendChild(square); and paste the following code directly above it:
const piece = board[row][col];
if (piece) {
const img = document.createElement('img');
const key = getPieceImageKey(piece);
img.src = PIECE_IMAGES[key];
img.alt = key;
square.appendChild(img);
}
What does this code do?
The getPieceImageKey function takes a piece object from chess.board() (which has color and type properties) and returns a key like wK or bP that matches the PIECE_IMAGES object.
Inside the loop, board[row][col] returns the piece on that square (or null if empty). When a piece exists, the code creates an <img> element pointing to the correct SVG URL and appends it to the square.
Before you save, what do you expect to see when the page refreshes?
- Save script.js and switch to your browser.
You should see a full 8x8 chessboard with all 32 pieces in their correct starting positions. White pieces on ranks 1 and 2, black pieces on ranks 7 and 8.
Pieces not showing up?
- Open the Console tab in DevTools. If you see 404 errors, double-check that the URLs in PIECE_IMAGES are copied exactly as shown above (they are case-sensitive).
- Make sure the piece-rendering code is inside the for loop, above boardElement.appendChild(square), not below it or outside the loop.
- help me debug why my chess piece images are not rendering
✔️ Awesome, I've got everything!
Great. Double check you've saved all three files.
ⓧ I'd like to double check the full code
Your index.html should look like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chess Game</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="game-container">
<div id="status"></div>
<div id="board"></div>
</div>
<script type="module" src="script.js"></script>
</body>
</html>
For style.css, confirm these chunks are present in order:
- Universal reset (*), body centering, #game-container flex column, and #status styling.
- #board grid, .square sizing, .square.light and .square.dark background colors, and .square img sizing.
For script.js, confirm these chunks are present in order:
- The import line and const chess = new Chess().
- The PIECE_IMAGES object with 12 SVG URLs.
- The boardElement and statusElement DOM references.
- The getPieceImageKey helper function.
- The renderBoard function creating 64 squares with piece images.
- The renderBoard() call at the bottom.
Your chessboard is alive on screen with all the pieces in position. Next up, you will make those pieces respond to clicks so two players can actually take turns moving them.
Add Click-to-Move
You have a beautiful chessboard rendered in the browser, but right now it is purely decorative. A game needs interaction.
In this step, you will wire up click events so two players can take turns moving pieces. You click a piece to select it, then click a destination to move it. chess.js handles all the rule enforcement automatically.
In this step, get ready to:
- Add click event listeners to every square on the board.
- Build selection logic so clicking a piece stores it as the active piece.
- Execute moves using chess.js with automatic pawn promotion.
Add click listeners and selection tracking
Each of the 64 squares needs to detect a click and decide what to do. The logic follows a two-click pattern: the first click selects a piece, and the second click chooses where to move it.
- In script.js, find the line that says const chess = new Chess(); and add this new variable directly below it:
let selectedSquare = null;
What does this variable do?
This variable tracks which square the player clicked first. When it is null, no piece is selected. When it holds a position like e2, the player has picked a piece and is choosing where to send it.
- Now scroll down to the renderBoard function. Find this line near the bottom of the for loop:
boardElement.appendChild(square);
- Add this line directly above boardElement.appendChild(square); so the event listener attaches before the square is added to the page:
square.addEventListener('click', () => handleSquareClick(position));
What does this listener do?
Every time a square is created in the loop, this attaches a click handler to it. The handler passes the square's algebraic position (like e2 or d7) to a function called handleSquareClick that you will write next.
- Add the handleSquareClick function below the closing brace of renderBoard by pasting the following code:
function handleSquareClick(position) {
// Ignore clicks if the game is over
if (chess.isGameOver()) return;
const piece = chess.get(position);
// If a piece is already selected, try to move it
if (selectedSquare) {
const move = attemptMove(selectedSquare, position);
if (move) {
selectedSquare = null;
renderBoard();
return;
}
// If clicked another own piece, switch selection
if (piece && piece.color === chess.turn()) {
selectedSquare = position;
return;
}
// Clicked invalid target, deselect
selectedSquare = null;
return;
}
// No piece selected yet, select if it belongs to current player
if (piece && piece.color === chess.turn()) {
selectedSquare = position;
}
}
What does this function do?
This function manages the two-click flow. First it checks if the game is still active.
- If no piece is selected (selectedSquare is null), it stores the clicked position only if it belongs to the current player.
- If a piece IS selected, it calls attemptMove to try the move. A legal move triggers a board re-render. An illegal click either switches selection or deselects.
- chess.turn() returns 'w' or 'b', so only the current player's pieces can be selected.
Create the move execution function
The handleSquareClick function above calls attemptMove but that function does not exist yet. It handles the actual chess move and includes automatic pawn promotion.
- Add the attemptMove function directly below handleSquareClick by pasting this code:
function attemptMove(from, to) {
try {
// Attempt the move, auto-promote pawns to queen
const move = chess.move({ from, to, promotion: 'q' });
return move;
} catch (e) {
return null;
}
}
What does this function do?
chess.move() throws an exception if the move is illegal. Wrapping it in a try/catch means illegal moves quietly return null instead of crashing the game.
The promotion: 'q' parameter tells chess.js to automatically promote any pawn that reaches the last rank into a queen. This covers a complex chess rule with a single parameter.
Test click-to-move in the browser
- Save script.js.
- Switch to your browser. Live Server should auto-refresh the page.
Before you click anything, what do you think will happen when you click a white pawn and then the square two rows ahead of it?
- Click any white pawn (second row from the bottom), then click the square two rows ahead of it.
The pawn jumps forward to the new square. The board re-renders with the pawn in its new position.
- Now try clicking a black piece. Nothing happens because it is white's turn.
- Click a white piece, then click a square that is not a legal move (like three rows ahead). Nothing happens because chess.js rejects the illegal move.
Pawn not moving when you click?
- Check that you added the addEventListener line inside the for loop in renderBoard, before boardElement.appendChild(square).
- Open DevTools (F12) and check the Console tab for errors. A common mistake is a typo in the function name handleSquareClick.
- Make sure let selectedSquare = null; is declared outside any function, near the top of the file.
Still stuck? Ask for help.
Try clicking a few more pieces and moving them around. Notice anything frustrating? There is no visual indicator showing which piece you selected. When a click does nothing, you have no idea why. The game works logically but the experience feels broken.
Why does it feel confusing?
This is intentional. The code is correct but the interface gives zero feedback. You cannot see which piece is selected. You cannot see which moves are legal. This gap between working logic and frustrating experience is exactly what the next step fixes.
✔️ Awesome, I've got everything!
Great. Double check you have saved script.js before moving on.
ⓧ I'd like to double check the full code
Your script.js is over 30 lines, so confirm each section is present in this order:
- The import statement and const chess = new Chess() at the top.
- let selectedSquare = null; directly below the chess instance.
- The PIECE_IMAGES object with all 12 SVG URLs.
- The boardElement and statusElement DOM references.
- getPieceImageKey function.
- renderBoard function with the addEventListener line before boardElement.appendChild(square).
- handleSquareClick function with selection and move logic.
- attemptMove function with promotion: 'q'.
- renderBoard(); call at the very bottom.
You now have a fully playable chess game. Two players can take turns, chess.js enforces every rule, and pawns promote automatically. But the experience is confusing because you cannot see what is happening under the hood. Next up, you will add visual feedback that transforms this from frustrating to polished.
Add Visual Feedback and Game Logic
Your chess game works logically. Pieces move, turns alternate, and illegal moves are rejected. But the experience is frustrating because you cannot see which piece is selected or why a click did nothing.
This step adds three layers of visual feedback: a yellow outline on the selected piece, green dots showing legal moves in Practice mode, and status messages that announce checkmate, stalemate, and whose turn it is.
In this step, get ready to:
- Add a selection highlight so you can see which piece you clicked.
- Add legal move highlighting that shows where a piece can go in Practice mode.
- Add game status detection that announces checkmate, stalemate, and draws.
Add selection and move highlighting styles
The highlight effects need CSS rules before the JavaScript can activate them. You will add styles for three states: a yellow outline for the selected piece, a green dot for legal moves, and a green border for capturable pieces.
- In style.css, find the .square.dark rule. Copy the following styles directly below it:
.square.selected {
outline: 3px solid #ffeb3b;
outline-offset: -3px;
}
.square.legal-move::after {
content: "";
position: absolute;
width: 20px;
height: 20px;
background-color: rgba(0, 128, 0, 0.5);
border-radius: 50%;
}
.square.legal-capture {
outline: 3px solid rgba(0, 128, 0, 0.6);
outline-offset: -3px;
}
What do these styles do?
- .square.selected draws a bright yellow outline around whichever piece you click.
- .square.legal-move::after creates a small green dot in the centre of empty squares the piece can move to.
- .square.legal-capture outlines enemy pieces that can be captured in green.
- Save style.css.
Nothing changes visually yet. These styles only activate once JavaScript adds the corresponding CSS classes to squares.
Now you need a function that applies these classes at the right time. The function will highlight the selected square, and in Practice mode, show all legal destinations.
- In script.js, find the line let selectedSquare = null; and add a new variable directly below it:
let practiceMode = false;
- Now add the updateHighlights() function below the attemptMove function by copying this code:
function updateHighlights() {
// Clear all previous highlights
const squares = document.querySelectorAll('.square');
squares.forEach(sq => {
sq.classList.remove('selected', 'legal-move', 'legal-capture');
});
if (!selectedSquare) return;
// Highlight the currently selected square
const selectedEl = document.querySelector(`[data-position="${selectedSquare}"]`);
if (selectedEl) {
selectedEl.classList.add('selected');
}
// In Practice mode, show legal move destinations
if (practiceMode) {
const moves = chess.moves({ square: selectedSquare, verbose: true });
moves.forEach(move => {
const targetEl = document.querySelector(`[data-position="${move.to}"]`);
if (targetEl) {
if (move.captured) {
targetEl.classList.add('legal-capture');
} else {
targetEl.classList.add('legal-move');
}
}
});
}
}
What does this function do?
- First it removes all highlight classes from every square, giving you a clean slate each time.
- If a square is selected, it applies the selected class to show the yellow outline.
- When practiceMode is true, it uses chess.moves({ square, verbose: true }) to fetch every legal destination and highlights each one.
The function exists, but nothing calls it yet. You need to call updateHighlights() whenever the selection changes.
- Replace your entire handleSquareClick function with this updated version that calls updateHighlights() after every selection change:
function handleSquareClick(position) {
if (chess.isGameOver()) return;
const piece = chess.get(position);
if (selectedSquare) {
const move = attemptMove(selectedSquare, position);
if (move) {
selectedSquare = null;
renderBoard();
return;
}
if (piece && piece.color === chess.turn()) {
selectedSquare = position;
updateHighlights();
return;
}
selectedSquare = null;
updateHighlights();
return;
}
if (piece && piece.color === chess.turn()) {
selectedSquare = position;
updateHighlights();
}
}
Where are the updateHighlights() calls?
There are three spots. Each time selectedSquare changes without a full board re-render, you call updateHighlights() to refresh the visual state. When a successful move triggers renderBoard(), the highlights get handled there instead.
- Finally, add an updateHighlights() call inside renderBoard() so the highlights refresh after every move. Find the closing braces at the end of renderBoard():
boardElement.appendChild(square);
}
}
}
- Add updateHighlights(); between the closing of the inner for-loop and the closing brace of the function, so it looks like this:
boardElement.appendChild(square);
}
}
updateHighlights();
}
- Save script.js and return to your browser.
- Click any white piece. You should see a bright yellow outline appear around the square you clicked.
- Click an empty square. The yellow outline disappears.
No yellow outline when you click?
- Check that .square.selected in your CSS uses outline (not border). A border would shift the layout of the grid.
- Make sure your handleSquareClick function calls updateHighlights() after setting selectedSquare. If you forgot the call, the class never gets added.
- Open DevTools (F12) and click the piece again. Look for the selected class on the square's div. If it's missing, the issue is in JavaScript. If it's present, the issue is in CSS.
Help me debug why my selection highlight isn't showing up.
Test legal move highlighting in Practice mode
The updateHighlights() function already contains the practice mode logic. It just needs practiceMode set to true to activate. You will add a proper UI toggle in the next step, but for now you can flip the variable to test it.
- In script.js, find this line near the top:
let practiceMode = false;
- Temporarily change false to true so the line looks like this:
let practiceMode = true;
- Save script.js and return to your browser.
- Click a white knight (the horse-shaped piece on b1 or g1). You should see green dots appear on the squares it can jump to.
- Click a white pawn. Green dots appear on the one or two squares directly ahead of it.
Why do captures get a different style?
Empty destination squares show a small green dot (legal-move). Squares containing an enemy piece show a green border (legal-capture) instead. This makes it instantly clear which moves are captures.
- Now change practiceMode back to false. You will wire it to a proper UI toggle in the next step:
let practiceMode = false;
- Save script.js.
Green dots not appearing?
- Confirm you changed practiceMode to true and saved the file. If it's still false, the practice mode block never runs.
- Make sure your .square.legal-move::after rule includes position: absolute. Without it, the dot won't appear inside the square correctly.
- Verify your .square rule has position: relative. The ::after pseudo-element needs a positioned parent.
Help me figure out why legal move dots aren't showing.
Detect and display game-over states
The game should tell players whose turn it is, warn when a king is in check, and announce checkmate or stalemate. The chess.js library already detects these conditions. You just need a function that reads them and writes the result to the status bar.
- In script.js, find the line let practiceMode = false; and add two new variables directly below it:
let player1Name = 'White';
let player2Name = 'Black';
- Now add the updateStatus() function below your updateHighlights() function by copying this code:
function updateStatus() {
// Check for checkmate first
if (chess.isCheckmate()) {
const winner = chess.turn() === 'w' ? player2Name : player1Name;
statusElement.textContent = `Checkmate! ${winner} wins!`;
return;
}
// Check for stalemate
if (chess.isStalemate()) {
statusElement.textContent = 'Stalemate! The game is a draw.';
return;
}
// Check for other draw conditions
if (chess.isDraw()) {
statusElement.textContent = 'Draw!';
return;
}
// Show whose turn it is, with check warning
const currentPlayer = chess.turn() === 'w' ? player1Name : player2Name;
const checkText = chess.inCheck() ? ' (Check!)' : '';
statusElement.textContent = `${currentPlayer}'s turn${checkText}`;
}
What does this function do?
- It checks game-ending conditions in priority order: checkmate first, then stalemate, then other draws.
- For checkmate, chess.turn() returns the side that CANNOT move. So if it's white's turn and checkmate, black won.
- If the game is still going, it shows the current player's name plus a "(Check!)" warning if their king is under attack.
- Finally, wire updateStatus() into renderBoard(). Find the line you just added:
updateHighlights();
}
- Add updateStatus(); directly after updateHighlights(); so the end of renderBoard() looks like this:
updateHighlights();
updateStatus();
}
- Save script.js and check your browser.
- You should now see "White's turn" in the status bar above the board.
- Make a white move. The status switches to "Black's turn".
Status bar stays blank?
- Make sure you added updateStatus(); inside renderBoard() AFTER the closing of the nested for-loops, not inside them.
- Check that player1Name and player2Name are declared at the top of the file. If they're undefined, the template string will show "undefined's turn".
Help me debug why the game status text is not updating.
Verify the full experience
Before you think about what you expect to see, temporarily enable Practice mode one more time to confirm everything works together.
- Change let practiceMode = false; to let practiceMode = true; and save.
What do you expect to see when you click a knight now? Think about what combination of selection highlight, legal move dots, and status text should appear together.
- Click the white knight on g1.
You should see all three feedback layers at once: a yellow outline on the knight, green dots on f3 and h3 (its legal destinations), and "White's turn" in the status bar.
- Change practiceMode back to false and save. The next step will add a proper UI toggle for this.
✔️ Awesome, I've got everything!
Make sure you saved both style.css and script.js with practiceMode set back to false.
ⓧ I'd like to double check the full code
Scroll up and confirm these code blocks are all present in your files, in order:
In style.css:
- The three new rules: .square.selected, .square.legal-move::after, and .square.legal-capture placed after the .square.dark rule.
In script.js:
- Variables at the top: selectedSquare, practiceMode (set to false), player1Name, and player2Name.
- renderBoard() ends with updateHighlights(); and updateStatus(); before its closing brace.
- The updated handleSquareClick() with updateHighlights() calls in three places.
- The full updateHighlights() function with both selection and practice mode logic.
- The full updateStatus() function checking checkmate, stalemate, draw, and check.
Your chess game now gives players clear feedback on every interaction. You can see what you selected, where you can move in Practice mode, and who won. Next up, you will add a start screen where players enter their names and choose between Normal and Practice mode.
Add Start Screen and Settings
Your chess game works, but it throws players straight into a game with no introduction. There is no way to enter your name, no way to choose Practice mode, and no way to start a fresh game once one is finished.
This step adds a start screen that greets players, lets them type their names, and pick between Normal and Practice mode. It transforms the experience from a raw board into something you would proudly hand to a friend.
In this step, get ready to:
- Build a start screen overlay with name inputs and a mode selector.
- Style the start screen so it covers the full page.
- Wire up the Start and New Game buttons to control the game flow.
Add the start screen HTML
The start screen needs to sit above the game board as a full-page overlay. When the player clicks Start, the overlay hides and the game appears.
- In index.html, replace everything between the opening <body> tag and the closing </body> tag with this new structure:
<div id="start-screen">
<div class="start-content">
<h1>Chess Game</h1>
<div class="input-group">
<label for="player1">Player 1 (White):</label>
<input type="text" id="player1" placeholder="White">
</div>
<div class="input-group">
<label for="player2">Player 2 (Black):</label>
<input type="text" id="player2" placeholder="Black">
</div>
<div class="input-group">
<label for="mode-select">Game Mode:</label>
<select id="mode-select">
<option value="normal">Normal</option>
<option value="practice">Practice</option>
</select>
</div>
<button id="start-btn">Start Game</button>
</div>
</div>
<div id="game-container" class="hidden">
<div id="status"></div>
<div id="board"></div>
<button id="new-game-btn">New Game</button>
</div>
<script type="module" src="script.js"></script>
What does this HTML do?
The start-screen div is a full-page overlay containing a form card. It has two text inputs for player names, a dropdown for Normal or Practice mode, and a Start Game button.
The game-container div now has the class hidden so it stays invisible until the player clicks Start. A New Game button sits below the board.
- Save index.html.
- Check your browser. You should see the raw text "Chess Game", two text inputs, a dropdown, and a "Start Game" button. They will look unstyled for now.
Seeing a blank page?
Make sure your <script type="module" src="script.js"></script> tag is still inside the body, after the game-container div. If it is missing, the page may not load correctly.
Also check that you did not accidentally delete the <head> section. The CSS link tag must still be there.
help me figure out why my chess game page is blank after editing the HTML
Style the start screen
The start screen elements are on the page, but they need styling to look like a centered card overlay. You will add CSS for the overlay, the card, the form inputs, and the buttons.
- In style.css, add the following styles below your existing .square img rule. Start with the overlay and card container:
#start-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #1a1a2e;
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
}
.start-content {
text-align: center;
background: #16213e;
padding: 40px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}
.start-content h1 {
margin-bottom: 24px;
font-size: 2rem;
}
What does this CSS do?
The #start-screen uses position: fixed to cover the entire viewport. It centers its child using flexbox.
The .start-content card gets a darker background, rounded corners, and a shadow to make it pop off the page.
- Save style.css and check your browser.
- You should see a dark card centered on the page with a large "Chess Game" heading.
- Next, add the form input styles directly below the code you just added:
.input-group {
margin-bottom: 16px;
text-align: left;
}
.input-group label {
display: block;
margin-bottom: 4px;
font-size: 0.9rem;
}
.input-group input,
.input-group select {
width: 100%;
padding: 8px 12px;
border: 1px solid #333;
border-radius: 6px;
background: #0f3460;
color: #eee;
font-size: 1rem;
}
What does this CSS do?
Each .input-group stacks a label above its input with consistent spacing. The inputs themselves get a dark blue background that matches the card theme.
- Finally, add the button styles and the hidden utility class:
#start-btn {
margin-top: 16px;
padding: 12px 32px;
font-size: 1.1rem;
background: #e94560;
color: #fff;
border: none;
border-radius: 6px;
cursor: pointer;
}
#start-btn:hover {
background: #c23152;
}
.hidden {
display: none !important;
}
#new-game-btn {
padding: 10px 24px;
font-size: 1rem;
background: #16213e;
color: #eee;
border: 1px solid #333;
border-radius: 6px;
cursor: pointer;
}
#new-game-btn:hover {
background: #0f3460;
}
What does this CSS do?
The Start button gets a bold red colour so it draws the eye. The .hidden class uses display: none !important to completely hide an element. JavaScript will toggle this class to show or hide the game.
The New Game button uses a subtle dark style so it does not compete with the board for attention.
- Save style.css and refresh your browser.
- You should see a polished start screen card with styled inputs, a dropdown, and a red Start Game button. The chessboard is no longer visible (it is hidden).
Start screen looks broken or unstyled?
Check that your CSS rules are placed after the existing .square img block, not inside it. A missing closing brace from an earlier rule will break everything below.
Also make sure the class name hidden in your CSS matches the class class="hidden" on the game-container div in your HTML.
help me debug why my start screen CSS is not displaying correctly
Wire up the Start and New Game buttons
The start screen looks great, but clicking the buttons does nothing yet. You need JavaScript to read the player names, set the game mode, and swap between the start screen and the game board.
- In script.js, find these two lines near the top of your file:
const boardElement = document.getElementById('board');
const statusElement = document.getElementById('status');
- Add these four new DOM references directly below them:
const startScreen = document.getElementById('start-screen');
const gameContainer = document.getElementById('game-container');
const startBtn = document.getElementById('start-btn');
const newGameBtn = document.getElementById('new-game-btn');
What do these references do?
These cache the start screen, game container, and both buttons so your event listeners can show, hide, and interact with them without searching the DOM repeatedly.
- Now scroll to the very bottom of script.js. Find the line that says:
renderBoard();
- Delete that line. The game should no longer render on page load because the start screen covers it. Replace it with this Start button listener:
startBtn.addEventListener('click', () => {
const p1Input = document.getElementById('player1').value.trim();
const p2Input = document.getElementById('player2').value.trim();
player1Name = p1Input || 'White';
player2Name = p2Input || 'Black';
const modeSelect = document.getElementById('mode-select');
practiceMode = modeSelect.value === 'practice';
startScreen.classList.add('hidden');
gameContainer.classList.remove('hidden');
chess.reset();
selectedSquare = null;
renderBoard();
});
What does this listener do?
When the player clicks Start, this reads their typed names (falling back to "White" and "Black" if empty). It checks the dropdown to set practiceMode to true or false.
Then it hides the start screen, shows the game container, resets the chess engine to the starting position, and renders the board fresh.
- Save script.js and refresh your browser.
- Type a name into the Player 1 field (e.g. Alice) and click Start Game.
- The start screen should disappear and the chessboard should appear with the status showing "Alice's turn".
Clicking Start does nothing?
Open your browser's DevTools console (press F12 then click the Console tab). If you see an error about Cannot read properties of null, the IDs in your HTML do not match the JavaScript. Double-check that the button has id="start-btn" in your HTML.
Also confirm you deleted the old renderBoard(); call. If it still runs on load, it may throw an error before the listener is attached.
help me debug why my Start Game button click is not working
- Now add the New Game button listener directly below the Start button listener you just added:
newGameBtn.addEventListener('click', () => {
chess.reset();
selectedSquare = null;
gameContainer.classList.add('hidden');
startScreen.classList.remove('hidden');
});
What does this listener do?
The New Game button resets the chess engine, clears any selection, hides the game board, and shows the start screen again. This lets players start over with new names or a different mode.
- Save script.js.
- In the browser, click Start Game to enter the game, then click New Game below the board.
- You should return to the start screen, ready to enter new names.
New Game button not visible or not working?
Make sure <button id="new-game-btn">New Game</button> sits inside game-container in your HTML, after the board div. If it is outside the container, it will not show when the game starts.
help me figure out why my New Game button is missing or broken
Time for the final test. Before you run through it, what do you expect to see when you refresh the page now?
- Refresh the page.
- Enter Alice for Player 1 and Bob for Player 2.
- Select Practice from the Game Mode dropdown.
- Click Start Game.
The start screen disappears. The board appears with the status reading "Alice's turn". Click a white piece and you see green legal-move dots (because you chose Practice mode). Your names are now driving the turn indicator.
✔️ Awesome, I've got everything!
Great. Double-check you have saved all three files before moving on.
ⓧ I'd like to double check the full code
Your files are longer than 30 lines each, so confirm each section is present by scrolling through your code:
- index.html: A start-screen div with the form card comes first in the body, followed by game-container with class hidden containing the status, board, and new-game-btn.
- style.css: After the existing board styles, you should have rules for #start-screen, .start-content, .start-content h1, .input-group, .input-group label, .input-group input/select, #start-btn, .hidden, and #new-game-btn.
- script.js: Four new DOM references (startScreen, gameContainer, startBtn, newGameBtn) after the existing two. No renderBoard() call at the bottom. Two event listeners (startBtn and newGameBtn) at the end of the file.
Your chess game now has a proper entrance. Players can introduce themselves, pick a difficulty mode, and jump into the game on their own terms. In the Secret Mission, you will push further by adding a captured pieces display and an undo button.
Secret mission
Add Captured Pieces and Undo
Real chess apps show you which pieces have been captured and let you take back mistakes. Can you add a captured pieces display and an undo button to your game?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The chess-game project folder on your Desktop (contains index.html, style.css, and script.js).
- The Live Server extension installed in VS Code.
Keep everything running
No action needed. Choose this if you're still actively building or want to keep playing your chess game.
- Your chess-game folder stays on your Desktop. Open index.html with Live Server any time to play.
- The Live Server extension remains in VS Code for future projects.
Pause - I'll come back to this later
Stop the Live Server to free up the port, but keep your files for later.
- In VS Code, click the Port: 5500 button in the bottom status bar to stop Live Server.
- Your chess-game folder stays on your Desktop. You can restart Live Server any time by right-clicking index.html and selecting Open with Live Server.
Delete - I don't want to use this again
Remove all project files and the extension to start fresh.
- Stop Live Server by clicking the Port: 5500 button in the VS Code status bar.
- Delete the project folder by running this command in your terminal:
macOS
rm -rf ~/Desktop/chess-game
Windows
Remove-Item -Recurse -Force ~\Desktop\chess-game
- To uninstall the Live Server extension, open the Extensions panel in VS Code (Ctrl+Shift+X), find Live Server, and click Uninstall.
Nice Work!
Nice Work!
Nice work! You just built a fully playable 2-player chess game from scratch using HTML, CSS, and JavaScript.
You've learned how to:
- Use CSS Grid to build an 8x8 board layout and style an interactive game UI with visual feedback for selections and legal moves.
- Integrate the chess.js library via ES Modules to handle complex game rules (castling, en passant, promotion, check, checkmate) without coding them yourself.
- Build a complete game state management system with click-to-move interaction, turn enforcement, Practice/Normal mode toggling, and automatic game-over detection.
- Secret Mission: You added a captured pieces display and an undo button that reverses the last full move.
Ready to quiz yourself?