1
Project Setup & Import Map
Instead of a bundler, we use a browser-native import map to load the
chess.js library directly from a CDN as an ES6 module. This keeps the setup
zero-config and blazing fast.
💡 Tip: Import maps are supported in all modern browsers (Chrome, Edge, Firefox, Safari). No build step needed!
<script type="importmap">
{
"imports": {
"chess.js": "https://esm.sh/chess.js@0.12.1"
}
}
</script>
This tells the browser: "when you see import { Chess } from 'chess.js', fetch it from esm.sh."
2
HTML Structure — Board & Info Panel
The DOM is intentionally minimal: a grid container for the 64 squares,
plus an info panel showing turn status, legal moves, errors, and a reset button.
<div class="game-container">
<div class="board" id="chessboard"></div>
<div class="info-panel">
<div class="status" id="gameStatus"></div>
<div class="moves-list" id="legalMoves"></div>
<div class="error" id="errorMsg"></div>
<button id="resetBtn">♻️ New game</button>
</div>
</div>
3
CSS Grid — The 8×8 Board
The board uses CSS Grid with grid-template-columns: repeat(8, 1fr).
Each square gets an aspect-ratio: 1/1 to stay perfectly square.
Alternating light/dark colours create the classic checkerboard.
.board {
display: grid;
grid-template-columns: repeat(8, 1fr);
width: min(70vw, 70vh, 560px);
height: min(70vw, 70vh, 560px);
}
.square.light { background: #f0d9b5; }
.square.dark { background: #b58863; }
⚠️ Note: Using min(70vw, 70vh, 560px) ensures the board never overflows the viewport on any screen size.
4
The ModernChess Class (Private Fields)
We encapsulate all game logic in a class with true private fields (the # prefix).
This keeps the internal state (#game, #selectedSquare) truly hidden from outside code.
class ModernChess {
#game;
#boardElement;
#selectedSquare = null;
#legalMovesForSelected = [];
constructor(boardElement) {
this.#game = new Chess();
this.#boardElement = boardElement;
}
}
💡 Private fields (#) are a real ES2022 feature. They provide runtime encapsulation — no workarounds needed.
5
Building the Board Dynamically
The 64 squares are generated in JavaScript using a double loop. Each square gets a
data attribute (data-square="e4") that maps it to standard algebraic notation.
Squares are also made draggable for drag-and-drop support.
#initBoardUI() {
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const file = String.fromCharCode(97 + col);
const rank = 8 - row;
const squareName = `${file}${rank}`;
const square = document.createElement('div');
square.dataset.square = squareName;
square.draggable = true;
}
}
}
6
Click & Drag-and-Drop Events
Two interaction modes are supported. Click-to-move: first click selects a piece,
second click attempts the move. Drag-and-drop: uses the HTML5 Drag API with
a hidden drag image for a clean feel.
#handleSquareClick(squareName) {
if (this.#selectedSquare) {
this.#tryMove(this.#selectedSquare, squareName);
this.#clearSelection();
return;
}
const piece = this.#game.get(squareName);
if (piece && piece.color === 'w') this.#setSelected(squareName);
}
boardElement.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', from);
e.dataTransfer.setDragImage(new Image(), 0, 0);
});
7
Move Validation with chess.js
The chess.js library handles all chess rules — legal moves, check,
checkmate, stalemate, and draws. We simply call game.move() and check the return
value to know if a move is valid.
#tryMove(from, to) {
const move = this.#game.move({ from, to, promotion: 'q' });
if (!move) {
this.#showMessage("Invalid move ❌");
return false;
}
this.#renderBoard();
this.#updateUI();
if (this.#game.game_over()) { }
return true;
}
💡 promotion: 'q' auto-promotes pawns to queens. In a full game you'd let the player choose, but this keeps the demo simple.
8
Rendering Pieces on the Board
After every move, the board is re-rendered. Each square is queried by its
data-square attribute, and piece images are loaded from Wikipedia's
public-domain chess piece set via chessboardjs.com.
#renderBoard() {
const board = this.#game.board();
for (let i = 0; i < 8; i++) {
for (let j = 0; j < 8; j++) {
const piece = board[i][j];
if (piece) {
const colorLetter = piece.color === 'w' ? 'w' : 'b';
const imgUrl = PIECE_IMG_URL
.replace('{piece}', `${colorLetter}${piece.type.toUpperCase()}`);
pieceImg.style.backgroundImage = `url('${imgUrl}')`;
}
}
}
}
9
AI Opponent — Random Move Engine
The AI is deliberately simple: it picks a random legal move for black.
The setTimeout adds a tiny delay so the UI updates before the AI responds,
making it feel more natural. This is where you'd plug in a real chess engine like Stockfish!
async #aiMove() {
if (this.#game.game_over() || this.#game.turn() !== 'b') return;
const moves = this.#game.moves({ verbose: true });
const random = moves[Math.floor(Math.random() * moves.length)];
this.#game.move({ from: random.from, to: random.to, promotion: 'q' });
this.#renderBoard();
this.#updateUI();
}
if (this.#game.turn() === 'b') setTimeout(() => this.#aiMove(), 100);
10
UI Updates & Legal Move Display
The info panel updates after every move: turn indicator, legal moves list,
check warnings, and error messages that auto-dismiss. Selected squares glow green
and legal target squares show a subtle dot indicator.
#updateUI() {
const turn = this.#game.turn();
let status = turn === 'w' ? '🤍 White (you)' : '🖤 Black (AI)';
if (this.#game.in_check()) status += ' ⚠️ Check!';
document.getElementById('gameStatus').innerHTML = status;
const moves = this.#game.moves({ verbose: true })
.map(m => m.san).slice(0, 16);
document.getElementById('legalMoves').innerHTML =
`📋 Moves: ${moves.join(', ') || 'none'}`;
}
🎮 LIVE DEMO
Play Against the AI
You play White. Click a piece, then click a target square — or drag & drop.
The AI responds automatically as Black.