Learn how to create a browser-based Snake Game using HTML, CSS, and JavaScript. Follow this step-by-step guide to build and customize your own game.
Table of Contents
Are you ready to build your very own browser-based Snake Game? This guide will show you how to create the classic Snake Game using simple HTML, CSS, and JavaScript. You don’t need to be an expert in coding—just some basic understanding of these three technologies is enough to get started.
Tools You’ll Need
- A Code Editor: Use a text editor like Visual Studio Code, Sublime Text, or Notepad++.
- A Browser: Any modern browser like Chrome, Firefox, or Edge will work.
Source Code
Step 1 (HTML Code):
To get started, we will first need to create a basic HTML file. The HTML is structured with a container element that contains the wrapper element, which in turn contains the canvas element, button, and score display. There is also an element for displaying the game's title and author.
Below is an explanation of the key components:
1. Document Type Declaration
<!DOCTYPE html>
- Declares the document as an HTML5 document.
2. <html>
Tag
<html lang="en">
- The
<html>
tag wraps the entire HTML document. - The
lang="en"
attribute specifies the language of the document as English.
3. <head>
Section
The <head>
contains metadata and links to external resources.
Meta Tags
<meta charset="UTF-8">
- Sets the character encoding to UTF-8 for supporting special characters.
<meta http-equiv="X-UA-Compatible" content="IE=edge">
- Ensures compatibility with modern rendering engines in browsers like Internet Explorer.
<meta name="viewport" content="width=device-width, initial-scale=1.0">
- Makes the page responsive by setting the width to the device's screen size.
Favicon
<link rel="shortcut icon" href="favicon.ico" />
- Links a small icon (
favicon.ico
) to display in the browser tab.
Title
<title>Snake Game</title>
- Sets the title of the page as "Snake Game."
Font Awesome Script
<script defer src="https://pro.fontawesome.com/releases/v5.10.0/js/all.js"
integrity="sha384-G/ZR3ntz68JZrH4pfPJyRbjW+c0+ojii5f+GYiYwldYU69A+Ejat6yIfLSxljXxD"
crossorigin="anonymous"></script>
- Loads Font Awesome icons for use on the page.
- The
defer
attribute ensures the script loads after the HTML is parsed.
Google Fonts
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@500;800&display=swap" rel="stylesheet">
- Links the "Poppins" font from Google Fonts.
CSS
<link rel="stylesheet" href="styles.css">
- Links an external stylesheet (
styles.css
) for styling the page.
4. <body>
Section
The <body>
contains the visible content of the page.
Main Container
<div class="container noselect">
- A
div
with the classcontainer
wraps the entire content. - The
noselect
class prevents text selection on this element.
Wrapper
<div class="wrapper">
- A
div
with the classwrapper
organizes the game elements.
Restart Button
<button id="replay">
<i class="fas fa-play"></i>
RESTART
</button>
- A button with the ID
replay
is used to restart the game. - Contains a play icon (
<i>
with the Font Awesome classfas fa-play
) and the text "RESTART."
Game Canvas
<div id="canvas">
</div>
- A
div
with the IDcanvas
acts as the game area where the Snake game will render.
Score UI
<div id="ui">
<h2>SCORE</h2>
<span id="score">00</span>
</div>
- Displays the score:
<h2>
shows the label "SCORE."<span>
with the IDscore
dynamically updates the score during the game.
Author Section
<div id="author">
<h1>SNAKE</h1> <span>by Faraz</span>
</div>
- A footer-like section displaying the game title "SNAKE" and the author's name "by Faraz."
5. JavaScript
<script src="script.js"></script>
- Links an external JavaScript file (
script.js
) for game logic.
After creating the files just paste the below codes into your file. Remember that you must save a file with the .html extension.
Step 2 (CSS Code):
Next, we will create our CSS file. Below is an explanation of its key parts:
1. Custom Font
@font-face {
font-family: "game";
src: url("https://fonts.googleapis.com/css2?family=Poppins:wght@500;800&display=swap");
}
- Defines a custom font called
game
. - The font is loaded from Google Fonts and uses the "Poppins" font family with weights 500 and 800.
2. Universal Selector
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
- Resets all padding and margin to
0
for consistent styling. - Sets
box-sizing: border-box
, ensuring padding and borders are included in element dimensions.
3. Button Focus Style
button:focus {
outline: 0;
}
- Removes the default focus outline when a button is selected.
4. HTML and Body
html,
body {
height: 100%;
font-family: "Poppins", sans-serif;
color: #6e7888;
}
body {
background-color: #222738;
display: flex;
justify-content: center;
align-items: center;
color: #6e7888;
}
- Sets the height of the
html
andbody
to 100% to allow full-page styling. - Applies the "Poppins" font and a text color of
#6e7888
. - The
body
is styled with a dark background color (#222738
) and uses Flexbox to center its content.
5. Canvas
canvas {
background-color: #181825;
}
- Sets the game canvas's background color to a darker shade (
#181825
).
6. Container
.container {
display: flex;
width: 100%;
height: 100%;
flex-flow: column wrap;
justify-content: center;
align-items: center;
}
- The
.container
is a Flexbox container. - Takes up the full width and height of the page.
- Aligns its children in the center both vertically and horizontally.
- Uses
flex-flow: column wrap
to arrange children in a column.
7. UI and Score
#ui {
display: flex;
align-items: center;
font-size: 10px;
flex-flow: column;
margin-left: 10px;
}
h2 {
font-weight: 200;
transform: rotate(270deg);
}
#score {
margin-top: 20px;
font-size: 30px;
font-weight: 800;
}
#ui
is a Flexbox container with column alignment for score elements.<h2>
(the "SCORE" label) is rotated 270 degrees.#score
(the score value) is styled with a larger font size and bold weight.
8. Prevent Text Selection
.noselect {
user-select: none;
}
- Disables text selection for elements with the
noselect
class.
9. Replay Button
#replay {
font-size: 10px;
padding: 10px 20px;
background: #6e7888;
border: none;
color: #222738;
border-radius: 20px;
font-weight: 800;
transform: rotate(270deg);
cursor: pointer;
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
#replay:hover {
background: #4cffd7;
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
- Styles the restart button:
- Rounded corners (
border-radius: 20px
). - Rotated 270 degrees.
- Smooth transition for hover effects.
- Rounded corners (
- On hover, the background color changes to
#4cffd7
.
10. Media Query
@media (max-width: 600px) {
#replay {
margin-bottom: 20px;
}
#replay,
h2 {
transform: rotate(0deg);
}
#ui {
flex-flow: row wrap;
margin-bottom: 20px;
}
#score {
margin-top: 0;
margin-left: 20px;
}
.container {
flex-flow: column wrap;
}
}
- Adjusts styles for screens narrower than 600px:
- Removes rotation for
#replay
and<h2>
. - Changes
#ui
to row layout. - Adjusts spacing for
#score
and#replay
.
- Removes rotation for
11. Author Section
#author {
width: 100%;
bottom: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 600;
color: inherit;
text-transform: uppercase;
padding-left: 35px;
}
#author span {
font-size: 10px;
margin-left: 20px;
color: inherit;
letter-spacing: 4px;
}
#author h1 {
font-size: 25px;
}
- Styles the footer-like author section:
- Centers content using Flexbox.
- Adds uppercase text for the title.
#author span
has letter spacing for better readability.
12. Wrapper
.wrapper {
display: flex;
flex-flow: row wrap;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
- The
.wrapper
arranges its children in rows with wrapping. - Centers content horizontally and vertically.
This will give our snake game an upgraded presentation. Create a CSS file with the name of styles.css and paste the given codes into your CSS file. Remember that you must create a file with the .css extension.
@font-face {
font-family: "game";
src: url("https://fonts.googleapis.com/css2?family=Poppins:wght@500;800&display=swap");
}
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
button:focus {
outline: 0;
}
html,
body {
height: 100%;
font-family: "Poppins", sans-serif;
color: #6e7888;
}
body {
background-color: #222738;
display: flex;
justify-content: center;
align-items: center;
color: #6e7888;
}
canvas {
background-color: #181825;
}
.container {
display: flex;
width: 100%;
height: 100%;
flex-flow: column wrap;
justify-content: center;
align-items: center;
}
#ui {
display: flex;
align-items: center;
font-size: 10px;
flex-flow: column;
margin-left: 10px;
}
h2 {
font-weight: 200;
transform: rotate(270deg);
}
#score {
margin-top: 20px;
font-size: 30px;
font-weight: 800;
}
.noselect {
user-select: none;
}
#replay {
font-size: 10px;
padding: 10px 20px;
background: #6e7888;
border: none;
color: #222738;
border-radius: 20px;
font-weight: 800;
transform: rotate(270deg);
cursor: pointer;
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
#replay:hover {
background: #a6aab5;
background: #4cffd7;
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
}
#replay svg {
margin-right: 8px;
}
@media (max-width: 600px) {
#replay {
margin-bottom: 20px;
}
#replay,
h2 {
transform: rotate(0deg);
}
#ui {
flex-flow: row wrap;
margin-bottom: 20px;
}
#score {
margin-top: 0;
margin-left: 20px;
}
.container {
flex-flow: column wrap;
}
}
#author {
width: 100%;
bottom: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 600;
color: inherit;
text-transform: uppercase;
padding-left: 35px;
}
#author span {
font-size: 10px;
margin-left: 20px;
color: inherit;
letter-spacing: 4px;
}
#author h1 {
font-size: 25px;
}
.wrapper {
display: flex;
flex-flow: row wrap;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
Step 3 (JavaScript Code):
Finally, we need to create a function in JavaScript. It includes variables for storing references to DOM elements, such as the replay button, score display, and canvas element. It also includes variables for storing game state and data, such as the snake, food, current hue, cell size, and game over status.
The code defines a number of helper functions and classes, such as the Vec class for representing 2D vectors and the isCollision
function for checking if two vectors are equal. It also includes functions for drawing the game grid, generating a random hue, and converting a hue, saturation, and lightness value to an RGB color.
The code also includes event listeners for handling user input, such as clicks on the replay button. It includes a function for updating the game state, as well as functions for rendering the game, drawing the snake and food, and handling collisions.
The code includes a game loop that uses the requestAnimationFrame
function to update and render the game at a consistent frame rate.
Create a JavaScript file with the name script.js and paste the given codes into your JavaScript file. Remember, you’ve to create a file with .js extension.
let dom_replay = document.querySelector("#replay");
let dom_score = document.querySelector("#score");
let dom_canvas = document.createElement("canvas");
document.querySelector("#canvas").appendChild(dom_canvas);
let CTX = dom_canvas.getContext("2d");
const W = (dom_canvas.width = 400);
const H = (dom_canvas.height = 400);
let snake,
food,
currentHue,
cells = 20,
cellSize,
isGameOver = false,
tails = [],
score = 00,
maxScore = window.localStorage.getItem("maxScore") || undefined,
particles = [],
splashingParticleCount = 20,
cellsCount,
requestID;
let helpers = {
Vec: class {
constructor(x, y) {
this.x = x;
this.y = y;
}
add(v) {
this.x += v.x;
this.y += v.y;
return this;
}
mult(v) {
if (v instanceof helpers.Vec) {
this.x *= v.x;
this.y *= v.y;
return this;
} else {
this.x *= v;
this.y *= v;
return this;
}
}
},
isCollision(v1, v2) {
return v1.x == v2.x && v1.y == v2.y;
},
garbageCollector() {
for (let i = 0; i < particles.length; i++) {
if (particles[i].size <= 0) {
particles.splice(i, 1);
}
}
},
drawGrid() {
CTX.lineWidth = 1.1;
CTX.strokeStyle = "#232332";
CTX.shadowBlur = 0;
for (let i = 1; i < cells; i++) {
let f = (W / cells) * i;
CTX.beginPath();
CTX.moveTo(f, 0);
CTX.lineTo(f, H);
CTX.stroke();
CTX.beginPath();
CTX.moveTo(0, f);
CTX.lineTo(W, f);
CTX.stroke();
CTX.closePath();
}
},
randHue() {
return ~~(Math.random() * 360);
},
hsl2rgb(hue, saturation, lightness) {
if (hue == undefined) {
return [0, 0, 0];
}
var chroma = (1 - Math.abs(2 * lightness - 1)) * saturation;
var huePrime = hue / 60;
var secondComponent = chroma * (1 - Math.abs((huePrime % 2) - 1));
huePrime = ~~huePrime;
var red;
var green;
var blue;
if (huePrime === 0) {
red = chroma;
green = secondComponent;
blue = 0;
} else if (huePrime === 1) {
red = secondComponent;
green = chroma;
blue = 0;
} else if (huePrime === 2) {
red = 0;
green = chroma;
blue = secondComponent;
} else if (huePrime === 3) {
red = 0;
green = secondComponent;
blue = chroma;
} else if (huePrime === 4) {
red = secondComponent;
green = 0;
blue = chroma;
} else if (huePrime === 5) {
red = chroma;
green = 0;
blue = secondComponent;
}
var lightnessAdjustment = lightness - chroma / 2;
red += lightnessAdjustment;
green += lightnessAdjustment;
blue += lightnessAdjustment;
return [
Math.round(red * 255),
Math.round(green * 255),
Math.round(blue * 255)
];
},
lerp(start, end, t) {
return start * (1 - t) + end * t;
}
};
let KEY = {
ArrowUp: false,
ArrowRight: false,
ArrowDown: false,
ArrowLeft: false,
resetState() {
this.ArrowUp = false;
this.ArrowRight = false;
this.ArrowDown = false;
this.ArrowLeft = false;
},
listen() {
addEventListener(
"keydown",
(e) => {
if (e.key === "ArrowUp" && this.ArrowDown) return;
if (e.key === "ArrowDown" && this.ArrowUp) return;
if (e.key === "ArrowLeft" && this.ArrowRight) return;
if (e.key === "ArrowRight" && this.ArrowLeft) return;
this[e.key] = true;
Object.keys(this)
.filter((f) => f !== e.key && f !== "listen" && f !== "resetState")
.forEach((k) => {
this[k] = false;
});
},
false
);
}
};
class Snake {
constructor(i, type) {
this.pos = new helpers.Vec(W / 2, H / 2);
this.dir = new helpers.Vec(0, 0);
this.type = type;
this.index = i;
this.delay = 5;
this.size = W / cells;
this.color = "white";
this.history = [];
this.total = 1;
}
draw() {
let { x, y } = this.pos;
CTX.fillStyle = this.color;
CTX.shadowBlur = 20;
CTX.shadowColor = "rgba(255,255,255,.3 )";
CTX.fillRect(x, y, this.size, this.size);
CTX.shadowBlur = 0;
if (this.total >= 2) {
for (let i = 0; i < this.history.length - 1; i++) {
let { x, y } = this.history[i];
CTX.lineWidth = 1;
CTX.fillStyle = "rgba(225,225,225,1)";
CTX.fillRect(x, y, this.size, this.size);
}
}
}
walls() {
let { x, y } = this.pos;
if (x + cellSize > W) {
this.pos.x = 0;
}
if (y + cellSize > W) {
this.pos.y = 0;
}
if (y < 0) {
this.pos.y = H - cellSize;
}
if (x < 0) {
this.pos.x = W - cellSize;
}
}
controlls() {
let dir = this.size;
if (KEY.ArrowUp) {
this.dir = new helpers.Vec(0, -dir);
}
if (KEY.ArrowDown) {
this.dir = new helpers.Vec(0, dir);
}
if (KEY.ArrowLeft) {
this.dir = new helpers.Vec(-dir, 0);
}
if (KEY.ArrowRight) {
this.dir = new helpers.Vec(dir, 0);
}
}
selfCollision() {
for (let i = 0; i < this.history.length; i++) {
let p = this.history[i];
if (helpers.isCollision(this.pos, p)) {
isGameOver = true;
}
}
}
update() {
this.walls();
this.draw();
this.controlls();
if (!this.delay--) {
if (helpers.isCollision(this.pos, food.pos)) {
incrementScore();
particleSplash();
food.spawn();
this.total++;
}
this.history[this.total - 1] = new helpers.Vec(this.pos.x, this.pos.y);
for (let i = 0; i < this.total - 1; i++) {
this.history[i] = this.history[i + 1];
}
this.pos.add(this.dir);
this.delay = 5;
this.total > 3 ? this.selfCollision() : null;
}
}
}
class Food {
constructor() {
this.pos = new helpers.Vec(
~~(Math.random() * cells) * cellSize,
~~(Math.random() * cells) * cellSize
);
this.color = currentHue = `hsl(${~~(Math.random() * 360)},100%,50%)`;
this.size = cellSize;
}
draw() {
let { x, y } = this.pos;
CTX.globalCompositeOperation = "lighter";
CTX.shadowBlur = 20;
CTX.shadowColor = this.color;
CTX.fillStyle = this.color;
CTX.fillRect(x, y, this.size, this.size);
CTX.globalCompositeOperation = "source-over";
CTX.shadowBlur = 0;
}
spawn() {
let randX = ~~(Math.random() * cells) * this.size;
let randY = ~~(Math.random() * cells) * this.size;
for (let path of snake.history) {
if (helpers.isCollision(new helpers.Vec(randX, randY), path)) {
return this.spawn();
}
}
this.color = currentHue = `hsl(${helpers.randHue()}, 100%, 50%)`;
this.pos = new helpers.Vec(randX, randY);
}
}
class Particle {
constructor(pos, color, size, vel) {
this.pos = pos;
this.color = color;
this.size = Math.abs(size / 2);
this.ttl = 0;
this.gravity = -0.2;
this.vel = vel;
}
draw() {
let { x, y } = this.pos;
let hsl = this.color
.split("")
.filter((l) => l.match(/[^hsl()$% ]/g))
.join("")
.split(",")
.map((n) => +n);
let [r, g, b] = helpers.hsl2rgb(hsl[0], hsl[1] / 100, hsl[2] / 100);
CTX.shadowColor = `rgb(${r},${g},${b},${1})`;
CTX.shadowBlur = 0;
CTX.globalCompositeOperation = "lighter";
CTX.fillStyle = `rgb(${r},${g},${b},${1})`;
CTX.fillRect(x, y, this.size, this.size);
CTX.globalCompositeOperation = "source-over";
}
update() {
this.draw();
this.size -= 0.3;
this.ttl += 1;
this.pos.add(this.vel);
this.vel.y -= this.gravity;
}
}
function incrementScore() {
score++;
dom_score.innerText = score.toString().padStart(2, "0");
}
function particleSplash() {
for (let i = 0; i < splashingParticleCount; i++) {
let vel = new helpers.Vec(Math.random() * 6 - 3, Math.random() * 6 - 3);
let position = new helpers.Vec(food.pos.x, food.pos.y);
particles.push(new Particle(position, currentHue, food.size, vel));
}
}
function clear() {
CTX.clearRect(0, 0, W, H);
}
function initialize() {
CTX.imageSmoothingEnabled = false;
KEY.listen();
cellsCount = cells * cells;
cellSize = W / cells;
snake = new Snake();
food = new Food();
dom_replay.addEventListener("click", reset, false);
loop();
}
function loop() {
clear();
if (!isGameOver) {
requestID = setTimeout(loop, 1000 / 60);
helpers.drawGrid();
snake.update();
food.draw();
for (let p of particles) {
p.update();
}
helpers.garbageCollector();
} else {
clear();
gameOver();
}
}
function gameOver() {
maxScore ? null : (maxScore = score);
score > maxScore ? (maxScore = score) : null;
window.localStorage.setItem("maxScore", maxScore);
CTX.fillStyle = "#4cffd7";
CTX.textAlign = "center";
CTX.font = "bold 30px Poppins, sans-serif";
CTX.fillText("GAME OVER", W / 2, H / 2);
CTX.font = "15px Poppins, sans-serif";
CTX.fillText(`SCORE ${score}`, W / 2, H / 2 + 60);
CTX.fillText(`MAXSCORE ${maxScore}`, W / 2, H / 2 + 80);
}
function reset() {
dom_score.innerText = "00";
score = "00";
snake = new Snake();
food.spawn();
KEY.resetState();
isGameOver = false;
clearTimeout(requestID);
loop();
}
initialize();
Final Output:
Conclusion:
In conclusion, building and playing the classic Snake Game using HTML, CSS, and JavaScript is a fun and rewarding project for anyone interested in game development or web development. With the step-by-step guide provided in this blog post, you can easily build the game from scratch and add your unique features to make it even more interesting. Whether you're a beginner or an experienced developer, this project is a great way to practice your skills and learn something new. So, give it a try and see what you can create!
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 😊