Run Dino

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:

Tools you’ll need:

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:

  1. Process input.
  2. Update the game state (positions, physics, timers).
  3. 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:

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:

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:

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:

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:

Example refactor idea:

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

2. Power-ups and pickups

3. Custom themes and skins

4. Leaderboards and saving scores

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:

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:

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:

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.