Home
> Chrome Dino Game > Create Your Own Dino Game in JavaScript
Create Your Own Dino Game in JavaScript – Full Masterclass Guide
The Chrome Dino Game is one of the most famous endless runners in the world. The good news? You don’t need a huge
engine or a big team to build something similar. With just HTML, CSS and JavaScript, you can create your own
Dino-style game that runs in the browser, works on desktop and mobile, and can be shared with anyone via a simple URL.
This guide walks you through the entire process, step by step: from basic setup and game loop architecture to
physics, collision detection, difficulty scaling, mobile controls, polish and deployment. By the end, you’ll not
only have a working game—but also understand how and why it works.
What you’ll build:
a simple but polished Dino-style endless runner with a jumping character, scrolling ground, obstacles, score,
increasing difficulty, day/night effect and support for both keyboard and touch controls.
1. Prerequisites and tools
You don’t need to be a senior developer to follow this tutorial, but you should be comfortable with:
- Basic HTML structure (<html>, <head>, <body>).
- Basic CSS (classes, positioning, backgrounds).
- Fundamental JavaScript (variables, functions, events, loops).
Tools you’ll need:
- A code editor (VS Code, Sublime, WebStorm or anything you like).
- A modern browser (Chrome, Edge, Firefox, etc.).
- Optional: a basic local server (VS Code Live Server or
npx serve). For simple cases, opening the HTML file directly also works.
2. Project structure
Let’s start with a simple project folder:
dino-game/
index.html
style.css
game.js
assets/
dino.png (optional)
cactus.png (optional)
ground.png (optional)
You can build the entire game using plain shapes and colors (no assets), which is perfect for learning.
Later, you can easily swap in sprite images.
3. Basic HTML markup
In index.html, set up a minimal document with a container for the game and references to your CSS and JS:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Dino Game</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="game-wrapper">
<div id="game">
<div id="ground"></div>
<div id="dino"></div>
</div>
<div class="hud">
<span id="score">00000</span>
<button id="restart" class="hidden">Restart</button>
</div>
</div>
<script src="game.js"></script>
</body>
</html>
The #game element is your “world”. The Dino and obstacles live inside it. The HUD (Heads-Up Display)
shows the score and restart button.
4. Styling the game world with CSS
Open style.css and start with a basic layout and simple visuals.
* {
box-sizing: border-box;
}
body {
margin: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #e5e7eb;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.game-wrapper {
position: relative;
width: 640px;
max-width: 100vw;
}
#game {
position: relative;
width: 100%;
height: 200px;
overflow: hidden;
background: #f9fafb;
border: 2px solid #111827;
border-radius: 6px;
}
#ground {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 24px;
background: repeating-linear-gradient(
-45deg,
#9ca3af 0,
#9ca3af 4px,
#6b7280 4px,
#6b7280 8px
);
}
#dino {
position: absolute;
left: 40px;
bottom: 24px; /* sits on the ground */
width: 32px;
height: 32px;
background: #111827;
border-radius: 4px;
}
.obstacle {
position: absolute;
bottom: 24px;
width: 18px;
height: 36px;
background: #111827;
border-radius: 3px;
}
.hud {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.5rem;
font-size: 0.9rem;
}
#score {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
#restart {
padding: 0.25rem 0.75rem;
font-size: 0.85rem;
border-radius: 4px;
border: 1px solid #111827;
background: #111827;
color: #f9fafb;
cursor: pointer;
}
#restart.hidden {
display: none;
}
You now have: a game area, a ground band, a simple square Dino and basic styling for obstacles and HUD. Next comes
the heart of any game: the game loop.
5. Understanding the game loop
Games run in a continuous loop. Each frame, you:
- Process input.
- Update the game state (positions, physics, timers).
- Render the new state to the screen.
In the browser, we use requestAnimationFrame for this. Open game.js:
const gameEl = document.getElementById('game');
const dinoEl = document.getElementById('dino');
const scoreEl = document.getElementById('score');
const restartBtn = document.getElementById('restart');
let lastTime = null;
let gameSpeed = 1;
let gameOver = false;
let score = 0;
function update(time) {
if (lastTime === null) {
lastTime = time;
requestAnimationFrame(update);
return;
}
const delta = (time - lastTime) / 1000; // seconds
lastTime = time;
if (gameOver) {
return;
}
// TODO: update Dino, obstacles, score, difficulty
requestAnimationFrame(update);
}
requestAnimationFrame(update);
delta is the time step between frames in seconds. This lets you update movement in a
frame-rate-independent way (e.g. position += speed * delta).
6. Representing the Dino: physics and state
Let’s model the Dino as a simple object in memory:
const GROUND_HEIGHT = 24;
const DINO_WIDTH = 32;
const DINO_HEIGHT = 32;
const GRAVITY = 1800; // pixels per second^2
const JUMP_VELOCITY = 650; // pixels per second
const gameHeight = 200; // from CSS
const dino = {
x: 40,
y: gameHeight - GROUND_HEIGHT - DINO_HEIGHT,
vy: 0,
isJumping: false,
isOnGround: true
};
function setDinoPosition() {
dinoEl.style.transform = `translate(${dino.x}px, ${dino.y}px)`;
}
We use transform: translate() instead of top/left adjustments because transforms are usually smoother
and more performant for animations.
Handling jump input
We want the Dino to jump when the player presses Space, ArrowUp or taps on mobile.
function handleJump() {
if (!dino.isOnGround || gameOver) return;
dino.vy = JUMP_VELOCITY;
dino.isJumping = true;
dino.isOnGround = false;
}
document.addEventListener('keydown', (e) => {
if (e.code === 'Space' || e.code === 'ArrowUp') {
handleJump();
}
});
Updating Dino position each frame
In the update loop, we apply gravity and move the Dino accordingly:
function updateDino(delta) {
dino.vy -= GRAVITY * delta;
dino.y += dino.vy * delta;
const groundY = gameHeight - GROUND_HEIGHT - DINO_HEIGHT;
if (dino.y < groundY) {
dino.y = groundY;
dino.vy = 0;
dino.isJumping = false;
dino.isOnGround = true;
}
setDinoPosition();
}
Now integrate this into the main update function:
function update(time) {
if (lastTime === null) {
lastTime = time;
requestAnimationFrame(update);
return;
}
const delta = (time - lastTime) / 1000;
lastTime = time;
if (gameOver) return;
updateDino(delta);
// Obstacles & score will go here
requestAnimationFrame(update);
}
7. Spawning and moving obstacles
A Dino runner isn’t a runner without obstacles. We’ll represent each obstacle as an object with x/y coordinates,
width and height.
const OBSTACLE_MIN_GAP = 260;
const OBSTACLE_MAX_GAP = 420;
const OBSTACLE_SPEED = 420; // base speed, scaled by gameSpeed
let obstacles = [];
let nextObstacleTime = 0;
function createObstacle() {
const obstacleEl = document.createElement('div');
obstacleEl.classList.add('obstacle');
gameEl.appendChild(obstacleEl);
const obstacle = {
el: obstacleEl,
x: gameEl.clientWidth,
y: gameHeight - GROUND_HEIGHT - 36,
width: 18,
height: 36
};
obstacleEl.style.transform = `translate(${obstacle.x}px, ${obstacle.y}px)`;
obstacles.push(obstacle);
}
function updateObstacles(delta) {
nextObstacleTime -= delta;
if (nextObstacleTime <= 0) {
createObstacle();
const gap = OBSTACLE_MIN_GAP + Math.random() * (OBSTACLE_MAX_GAP - OBSTACLE_MIN_GAP);
nextObstacleTime = gap / (OBSTACLE_SPEED * gameSpeed);
}
obstacles.forEach((obstacle) => {
obstacle.x -= OBSTACLE_SPEED * gameSpeed * delta;
obstacle.el.style.transform = `translate(${obstacle.x}px, ${obstacle.y}px)`;
});
obstacles = obstacles.filter((obstacle) => {
if (obstacle.x + obstacle.width < 0) {
obstacle.el.remove();
return false;
}
return true;
});
}
And in your main update loop:
function update(time) {
if (lastTime === null) {
lastTime = time;
requestAnimationFrame(update);
return;
}
const delta = (time - lastTime) / 1000;
lastTime = time;
if (gameOver) return;
updateDino(delta);
updateObstacles(delta);
updateScore(delta);
checkCollisions();
requestAnimationFrame(update);
}
8. Collision detection (Dino vs obstacles)
We’ll use simple axis-aligned bounding box (AABB) collision detection. Each frame, we check whether the Dino
rectangle overlaps any obstacle rectangle.
function rectsOverlap(r1, r2) {
return (
r1.x < r2.x + r2.width &&
r1.x + r1.width > r2.x &&
r1.y < r2.y + r2.height &&
r1.y + r1.height > r2.y
);
}
function checkCollisions() {
const dinoRect = {
x: dino.x,
y: dino.y,
width: DINO_WIDTH,
height: DINO_HEIGHT
};
for (const obstacle of obstacles) {
const obstacleRect = {
x: obstacle.x,
y: obstacle.y,
width: obstacle.width,
height: obstacle.height
};
if (rectsOverlap(dinoRect, obstacleRect)) {
handleGameOver();
break;
}
}
}
function handleGameOver() {
gameOver = true;
restartBtn.classList.remove('hidden');
}
You now have a complete core loop: the Dino runs, jumps, obstacles spawn and move, and collisions end the game.
9. Scoring and difficulty scaling
To keep the game engaging, we’ll:
- Increase the score over time.
- Gradually increase game speed to add difficulty.
const SCORE_PER_SECOND = 40;
const SPEED_INCREASE_RATE = 0.15; // per second
function updateScore(delta) {
score += SCORE_PER_SECOND * delta * gameSpeed;
const displayScore = String(Math.floor(score)).padStart(5, '0');
scoreEl.textContent = displayScore;
// Gradually increase game speed
gameSpeed += SPEED_INCREASE_RATE * delta;
}
This gives you the classic endless-runner feeling: early game is relaxed, mid-game is focused, and late game becomes
intense.
10. Restarting the game
After game over, the player should be able to restart easily.
function resetGame() {
obstacles.forEach(o => o.el.remove());
obstacles = [];
score = 0;
gameSpeed = 1;
scoreEl.textContent = '00000';
dino.y = gameHeight - GROUND_HEIGHT - DINO_HEIGHT;
dino.vy = 0;
dino.isOnGround = true;
setDinoPosition();
gameOver = false;
restartBtn.classList.add('hidden');
lastTime = null; // reset time so delta doesn't spike
requestAnimationFrame(update);
}
restartBtn.addEventListener('click', resetGame);
document.addEventListener('keydown', (e) => {
if (e.code === 'Space' && gameOver) {
resetGame();
}
});
11. Adding touch controls for mobile
Many players will try your Dino game on phones. Let’s map tap events to jump and restart:
gameEl.addEventListener('touchstart', (e) => {
e.preventDefault();
if (gameOver) {
resetGame();
} else {
handleJump();
}
}, { passive: false });
Now your game responds to both keyboard and touch events, making it playable on most devices.
12. Simple day/night cycle (visual polish)
To mimic the Chrome Dino vibe, we can gradually flip the background between light and dark based on score or time.
let isNight = false;
let nextToggleScore = 500; // first flip around 500 pts
function updateDayNight() {
if (score >= nextToggleScore) {
isNight = !isNight;
nextToggleScore += 500;
if (isNight) {
gameEl.style.background = '#111827';
gameEl.style.borderColor = '#e5e7eb';
} else {
gameEl.style.background = '#f9fafb';
gameEl.style.borderColor = '#111827';
}
}
}
Call this inside updateScore or directly in the main update loop:
function updateScore(delta) {
score += SCORE_PER_SECOND * delta * gameSpeed;
scoreEl.textContent = String(Math.floor(score)).padStart(5, '0');
gameSpeed += SPEED_INCREASE_RATE * delta;
updateDayNight();
}
13. Optional: adding sound effects
Sound is a small detail that makes a big difference. You can add:
- Jump sound.
- Score milestone “ping”.
- Game over sound.
Example using simple HTML audio:
const jumpAudio = new Audio('assets/jump.wav');
const hitAudio = new Audio('assets/hit.wav');
function handleJump() {
if (!dino.isOnGround || gameOver) return;
dino.vy = JUMP_VELOCITY;
dino.isJumping = true;
dino.isOnGround = false;
jumpAudio.currentTime = 0;
jumpAudio.play();
}
function handleGameOver() {
gameOver = true;
hitAudio.currentTime = 0;
hitAudio.play();
restartBtn.classList.remove('hidden');
}
Browser note: Most browsers require a user interaction (click/tap/keypress) before
audio can play. That’s fine here because jump/game over only happen after input.
14. Improving visuals (sprites and animation)
Our Dino is still just a square. To make it more visually interesting:
- Use a sprite sheet (running animation frames) for the Dino.
- Switch background positions over time to create “running legs”.
- Create different cactus heights and widths.
Example CSS for a sprite-based Dino:
#dino {
position: absolute;
left: 40px;
bottom: 24px;
width: 44px;
height: 48px;
background-image: url('assets/dino-sprite.png');
background-repeat: no-repeat;
background-position: 0 0;
}
Then, in JavaScript, you could update backgroundPositionX every few frames while the Dino is on the
ground, cycling through animation frames. Even a simple 2-frame run loop can make the game feel alive.
15. Performance considerations
Even small games benefit from good performance practices:
- Use transforms instead of top/left: you’re already doing this with
transform: translate.
- Reuse DOM nodes where possible: instead of constantly creating/removing elements, you can recycle them.
- Keep logic simple inside the loop: avoid heavy computations each frame.
- Store references once: don’t query the DOM repeatedly for the same elements.
For most Dino clones, the game is lightweight enough that modern browsers handle it easily—even on mobile.
16. Code organization and refactoring
Once your prototype works, it’s a great time to clean up the structure:
- Group related constants together.
- Split large functions into smaller helpers.
- Consider splitting code into modules (ES modules) if you grow the game.
Example refactor idea:
dino.js – handles Dino state and movement.
obstacles.js – manages obstacle spawning and movement.
score.js – scoring and difficulty scaling.
game.js – main loop and orchestration.
Start simple in a single file. Refactor into modules once you understand the whole flow. It’s easier to reorganise
working code than to over-engineer from the start.
17. Extending your Dino game: ideas for advanced features
Once the base game feels solid, you can expand it into a richer experience. Here are some directions you can take:
1. Multiple obstacle types
- Short and tall cacti.
- Birds (flying enemies) at different heights.
- Double and triple obstacle clusters.
2. Power-ups and pickups
- Temporary invincibility.
- Score multipliers.
- Slow-motion power-ups that temporarily reduce gameSpeed.
3. Custom themes and skins
- Change Dino color or costume after reaching milestones.
- Unlock visual themes based on high scores.
4. Leaderboards and saving scores
- Store a local high score in
localStorage.
- Integrate with a backend API to track global scores (similar to Run Dino Run leaderboards).
18. Saving and loading high scores (localStorage)
A simple way to add progression is to track the best score on a specific device using localStorage.
let bestScore = Number(localStorage.getItem('dinoBestScore') || 0);
function updateBestScore() {
if (score > bestScore) {
bestScore = score;
localStorage.setItem('dinoBestScore', String(bestScore));
}
}
function handleGameOver() {
gameOver = true;
updateBestScore();
restartBtn.classList.remove('hidden');
}
You can also display the best score next to the current score to motivate players.
19. Deploying your Dino game
One of the best parts of a JavaScript Dino game is how easy it is to share:
- Upload the files to any static hosting (Netlify, GitHub Pages, Vercel, your own server).
- Ensure relative paths (like
./assets/...) are correct.
- Share the URL with friends or embed it on your main site (like your Run Dino Run homepage).
20. Turning your Dino game into a “Run Dino Run”-style project
To level up your game towards a full product like Run Dino Run, consider:
- User accounts / nicknames: so players can tag their high scores.
- Global and monthly leaderboards: encourage competition.
- Responsive layout: adapt UI for phones, tablets and desktops.
- Progression systems: achievements, unlockables, cosmetic rewards.
- PWA support: allow players to “install” your Dino game on their home screen and play offline.
Every one of these features builds on the same core you’ve just implemented: game loop, input, physics, obstacles
and score.
21. Summary: what you’ve learned
By following this guide, you’ve gone through the full process of designing and coding a Dino-style endless runner
in JavaScript:
- Setting up HTML, CSS and JavaScript files for a small web game.
- Building a game loop with
requestAnimationFrame and time-based updates.
- Implementing jump physics with velocity and gravity.
- Spawning and moving obstacles with configurable gaps.
- Detecting collisions with simple rectangle overlap checks.
- Adding scoring, speed scaling and day/night visuals.
- Supporting keyboard and touch controls for cross-device play.
- Adding restart functionality, sound effects and basic polish.
- Planning advanced features such as power-ups, skins, leaderboards and deployment.
From here, you can keep iterating until your Dino game feels as fast, smooth and addictive as the original Chrome
Dino—or even build your own twist that’s unique to your brand, like Run Dino Run.