
- Step 1: Plan Your Maze
- Step 2: Setting up the maze container
- Step 3: Generating the Full Maze Map
- Step 4: Adding Event Listeners to Detect Key Presses
- Next Step: Navigating the Maze with Smooth Transitions
- Exploring Further: Taking Your Maze Game to the Next Level
Are you looking for a fun and challenging project to improve your front-end coding skills? In this tutorial, we’ll show you how to use HTML, CSS, and Javascript to design and build your own mini maze game. If you are ready to dive in and create your own fun and challenging maze game, Let’s get started!
Step 1: Plan Your Maze
To start creating your maze game, first of all, you need to design the maze itself. The basic maze design should consist of the following key elements:
- 1 x starting point
- 1 x endpoint
- Pathways in between
For this tutorial, we’ll use a simple 3×3 grid layout as an example to keep everything easy to follow from the basics.
Here’s the maze design:

Step 2: Setting up the maze container
Once we have the map design prepared, we can start to set up the maze area in HTML and CSS. We will start by creating a 3×3 grid container and naming it as .maze_container
to hold the maze.
<style>
.maze_container{
display: grid;
grid-template-columns: repeat(3, 50px);
grid-template-rows: repeat(3, 50px);
}
</style>
<div class="maze_container"></div>
In this example, we will use dimension 50x50px
for each grid item.
To understand more about how a grid system works, you may refer to our previous tutorial at CSS Grid: A Step-by-Step Guide with Examples
Step 3: Generating the Full Maze Map
3.1 JavaScript & HTML
Before diving into the coding part to generate the full maze, Here’s what we need to do:
- Plan out the class names for maze elements such as walls, pathways, start-point, and end-point.
- Slice out the images that will be used for the maze.
- Identify any reusable items. (In this example:
path_1
)

![]() | ![]() | ![]() | ![]() | ![]() | ![]() |
path_1 | path_2 | path_3 | path_4 | path_5 | end |
Once you have all of that in place, you can begin to create the maze map by using array:
const maze = [
["path_1", "path_2", "path_3"],
["wall", "path_4", "wall"],
["path_1", "path_5", "end"],
];
Here are the complete details of the JavaScript code to generate the 3×3 maze layout in HTML:
// Select the HTML container where the maze will be displayed
const container = document.querySelector(".maze_container");
// Define the dimension of each grid item in pixels
const gridSize = 50;
// Define the maze structure as a 2D array with path, wall and end tiles
const maze = [
["path_1", "path_2", "path_3"],
["wall", "path_4", "wall"],
["path_1", "path_5", "end"],
];
// Define the starting position of the player
const startpoint = {x:0, y:0};
// Generate a 3x3 loop to create grid items
for (let i = 0; i < maze.length; i++) {
for (let j = 0; j < maze[i].length; j++) {
// Create a new HTML element for the grid item
// Add a CSS class to the grid item based on the maze structure
// Append the grid item to the container
const cell = document.createElement("div");
cell.classList.add(maze[i][j]);
container.appendChild(cell);
//initiate the player's starting position
if(i == startpoint.x && j == startpoint.y){
cell.classList.add("player");
}
}
}
In this example, we will add in a .player
class to indicate the player’s current position. (refer to line 30)
Result:

3.2 CSS
Next, we will style the maze by:
– Define the background for each path
– Style the player’s avatar (gameplay status: .player::before
, winning status: .player.win::before
)
/*Style the main avatar*/
.player::before{
position: absolute;
content: "";
width: 10px;
height: 10px;
border-radius: 50%;
background: red;
top: calc(50% - 5px);
left: calc(50% - 5px);
}
/*Style the main avatar- winning effect*/
.player.win::before{
background: purple;
}
/*Style all the pathways*/
.path_1, .path_2, .path_3, .path_4, .path_5, .end{
background-size: cover;
width: 50px;
height: 50px;
position: relative;
}
.path_1{
background-image: url(img/path_1.png);
}
.path_2{
background-image: url(img/path_2.png);
}
.path_3{
background-image: url(img/path_3.png);
}
.path_4{
background-image: url(img/path_4.png);
}
.path_5{
background-image: url(img/path_5.png);
}
.end{
background-image: url(img/end.png);
}
Result:
Step 4: Adding Event Listeners to Detect Key Presses
Once we have styled the maze, we can move on to the final step, which is adding an event listener to detect key presses on the keyboard. This will allow us to move the player around the maze.
We will be using JavaScript to detect the arrow key presses (up, down, left, right) and update the player’s position accordingly.
Here’s an example:
//Initiate the starting row & column position
let playerRow = startpoint.x;
let playerCol = startpoint.y;
//Add an event listener to listen for key-down events
document.addEventListener("keydown", (event) => {
const key = event.key;
//Create a temporary cell to select the current player's container
const tempplayerCell = document.querySelector(".player");
//Check if the player has already won the game, if yes, do nothing. else, proceed to detect the key pressed
if (!tempplayerCell.classList.contains("win")) {
if (key === "ArrowUp" && playerRow > 0 && maze[playerRow - 1][playerCol] !== "wall") {
playerRow--;
}
else if (key === "ArrowDown" && playerRow < maze.length - 1 && maze[playerRow + 1][playerCol] !== "wall") {
playerRow++;
}
else if (key === "ArrowLeft" && playerCol > 0 && maze[playerRow][playerCol - 1] !== "wall") {
playerCol--;
}
else if (key === "ArrowRight" && playerCol < maze[0].length - 1 && maze[playerRow][playerCol + 1] !== "wall") {
playerCol++;
}
//Create a new cell to hold where the player moved to
const newPlayerCell = container.children[playerRow * maze[0].length + playerCol];
//Remove the player class from the old cell and add it to the new cell
tempplayerCell.classList.remove("player");
newPlayerCell.classList.add("player");
//check if the player has reached the end-point and won the game, if yes, apply winning effect
if (newPlayerCell.classList.contains("end")) {
newPlayerCell.classList.add("win");
}
}
});
When the user presses an arrow key, the code checks for the following scenarios to determine if the player can move in that direction based on their current location:
- If the player pressed the up arrow key (key === “ArrowUp”):
- And the player is not already in the top row.
- And there is no wall in the cell above the player’s current position.
- Then, decrement the playerRow variable.
- If the player pressed the down arrow key (key === “ArrowDown”):
- And the player is not already in the bottom row.
- And there is no wall in the cell below the player’s current position.
- Then, increment the playerRow variable.
- If the player pressed the left arrow key (key === “ArrowLeft”):
- And the player is not already in the leftmost column.
- And there is no wall in the cell to the left of the player’s current position.
- Then, decrement the playerCol variable.
- If the player pressed the right arrow key (key === “ArrowRight”):
- And the player is not already in the rightmost column.
- And there is no wall in the cell to the right of the player’s current position.
- Then, increment the playerCol variable.
Result preview:

For full demo: click me
Congratulations! You have finished the basic tutorial on how to create a maze. It was pretty easy, wasn’t it? If you’re eager to learn more, let’s move on to the slightly advanced version on the next step!
Next Step: Navigating the Maze with Smooth Transitions
For this version, we aim to improve the gameplay experience. Instead of just adding and removing the .player
class to move the avatar, we’ll implement a new navigation system that provides a more immersive experience by fixing the position of the avatar and moving the entire maze container accordingly. Here’s the example:

To achieve this, we will add a new div container (.main_viewport
) to wrap the existing maze’s container (.maze_container
). Then, we will change the existing maze’s container CSS to position: absolute;
so that it will allow us to move the entire maze by adjusting its top
and left
values later on.
Changes from HTML
<div class="main_viewport">
<div class="maze_container"></div>
</div>
Changes from CSS
To hide any maze area outside the viewport, we will apply overflow:hidden
to .main_viewport
(see code below: line 9).
To ensure the main avatar stays within the viewport while navigating, we will abandon the previous workaround for the .player class. Instead, we will move the CSS for the avatar to the .main_viewport::before
, which will be positioned absolutely at the center of the viewport at all times. (see code below: 33-42)

For detailed changes, please refer to the highlighted code below
<!DOCTYPE html>
<html>
<head>
<style>
/*Create a container to keep the player’s viewport static*/
/*Use overflow: hidden to mask the area outside of the viewport*/
.main_viewport {
position: relative;
overflow: hidden;
}
/* Apply a radial gradient effect to enhance viewport's design */
.main_viewport::after {
position: absolute;
content: "";
background: radial-gradient(circle, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 43%, rgba(0, 0, 0, 1) 100%);
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 2;
}
/*Use position absolute to make the entired maze moveable*/
/*Apply transition effect for seamless animation*/
.maze_container {
position: absolute;
transition: 0.3s linear all;
display: grid;
}
/*Style the main avatar*/
.main_viewport::before {
position: absolute;
content: "";
width: 10px;
height: 10px;
left: calc(50% - 5px);
top: calc(50% - 5px);
z-index: 2;
background-color: red;
}
/*Style the winning effect*/
.main_viewport.win::before{
background-color: purple;
}
/*Style all the Pathways*/
.path_1,
.path_2,
.path_3,
.path_4,
.path_5,
.end {
background-size: cover;
position: relative;
width: 100%;
height: 100%;
}
.path_1{
background-image: url(img/path_1.png);
}
.path_2{
background-image: url(img/path_2.png);
}
.path_3{
background-image: url(img/path_3.png);
}
.path_4{
background-image: url(img/path_4.png);
}
.path_5{
background-image: url(img/path_5.png);
}
.end{
background-image: url(img/end.png);
}
</style>
</head>
<body>
<div class="main_viewport">
<div class="maze_container"></div>
</div>
<script>
// Select the HTML container where the viewport will be held
const main = document.querySelector(".main_viewport");
// Select the HTML container where the maze will be displayed
const container = document.querySelector(".maze_container");
// Define the dimension of each grid item in pixels
const gridSize = 50;
// Define the maze structure as a 2D array with paths, walls and end point
const maze = [
["path_1", "path_2", "path_3"],
["wall", "path_4", "wall"],
["path_1", "path_5", "end"],
];
// Define the desired start and end point
const startpoint = { x: 0, y: 0 };
const endpoint = { x: 2, y: 2 };
//Generate CSS dynamically
container.style.gridTemplateColumns = 'repeat('+ maze[0].length +', '+gridSize+'px)';
container.style.gridTemplateRows = 'repeat('+maze.length+', '+gridSize+'px)';
main.style.width = gridSize + "px";
main.style.height = gridSize + "px";
container.style.left = - (startpoint.x * gridSize) + "px";
container.style.top = - (startpoint.y * gridSize) + "px";
// Generate a 3x3 loop to create grid items
for (let i = 0; i < maze.length; i++) {
for (let j = 0; j < maze[i].length; j++) {
const cell = document.createElement("div");
cell.classList.add(maze[i][j]);
container.appendChild(cell);
}
}
//Initiate the starting row & column position
let playerRow = startpoint.x;
let playerCol = startpoint.y;
//Initiate the maze's container position
let topPosition = container.offsetTop;
let leftPosition = container.offsetLeft;
//Add an event listener to listen for key-down events
document.addEventListener("keydown", (event) => {
const key = event.key;
if (!container.classList.contains("win")) {
const key = event.key;
if (key === "ArrowUp") {
if (playerRow > 0 && maze[playerRow - 1][playerCol] !== "wall") {
playerRow--;
topPosition += gridSize;
container.style.top = topPosition + "px";
}
}
else if (key === "ArrowDown") {
if (playerRow < maze.length - 1 && maze[playerRow + 1][playerCol] !== "wall") {
playerRow++;
topPosition -= gridSize;
container.style.top = topPosition + "px";
}
}
else if (key === "ArrowLeft") {
if (playerCol > 0 && maze[playerRow][playerCol - 1] !== "wall") {
playerCol--;
leftPosition += gridSize;
container.style.left = leftPosition + "px";
}
}
else if (key === "ArrowRight") {
if (playerCol < maze[0].length - 1 && maze[playerRow][playerCol + 1] !== "wall") {
playerCol++;
if (leftPosition >= -(gridSize * endpoint.x)) {
leftPosition -= gridSize;
leftPosition == -(gridSize * endpoint.x);
container.style.left = leftPosition + "px";
}
}
}
//check if the player has reached the end-point and won the game, if yes, apply winning effect
if (maze[playerRow][playerCol] === "end") {
setTimeout(() => {
main.classList.add("win");
container.style.left = -(gridSize * endpoint.x) + "px";
return false;
}, 100);
}
}
});
</script>
</body>
</html>
Changes from JavaScript
In this example, we have made changes to some CSS values so that they can be assigned dynamically based on the custom dimensions of the grid items (refer to row 113-120). This allows the styling to adjust automatically to fit any custom grid size.
To keep the maze container moves based on the user’s input, in addition to the previous logic in the key-down events, will be adding the following code:
(The value 50px
is define from const gridSize
)
- If the player pressed the up arrow key (key === “ArrowUp”):
topPosition += gridSize;
container.style.top = topPosition + "px";
- “Move the entire maze down by
50px
by increasing the CSS ‘top
‘ value.
- “Move the entire maze down by
- If the player pressed the down arrow key (key === “ArrowDown”):
topPosition -= gridSize;
container.style.top = topPosition + "px";
- “Move the entire maze up by
50px
by decreasing the CSS ‘top
‘ value. (Th
- “Move the entire maze up by
- If the player pressed the left arrow key (key === “ArrowLeft”):
leftPosition += gridSize;
container.style.left = leftPosition + "px";
- “Move the entire maze right by
50px
by increasing the CSS ‘left
‘ value. (Th
- “Move the entire maze right by
- If the player pressed the right arrow key (key === “ArrowRight”):
leftPosition -= gridSize;
container.style.left = leftPosition + "px";
- “Move the entire maze left by
50px
by decreasing the CSS ‘left
‘ value. (Th
- “Move the entire maze left by
A full demo for this version: click me
Exploring Further: Taking Your Maze Game to the Next Level
Congratulations on completing the tutorial for creating a maze game! Now, you can unleash your creativity and modify the code to create your own maze game with more complex paths, you can also add animations and customize your avatar to enhance the player’s experience and increase the difficulty to take your game to the next level.
You can access my version of the mini maze by clicking on the following link:
click me for full demo



This version is using CSS box-shadow to create a pixelated avatar and animate by CSS. If you are interested in learning more about how to create pixelated art by CSS, click me
Keep exploring and experimenting with your code to make your maze game even more exciting! Please leave a comment and let me know if you found this article helpful or have any questions. And don’t forget to share this article with your friends or colleagues who might find it useful. Good luck and happy coding!
Leave a Reply