Build an Interactive 3D Solar System

Build a 3D solar system in the browser with orbiting planets and click interaction.

Introduction

30 Second Summary

Every time you spin a 3D product on a website or explore an interactive map, there is a 3D engine running behind the scenes. Three.js is the most popular library for building these experiences directly in the browser.

In this project, you will build a full-screen interactive 3D solar system. A glowing sun sits at the center while colorful planets orbit around it, and you can click any planet to see its name.

What You'll Build

You will create a browser-based 3D solar system using Three.js and Vite, complete with animated orbits, realistic lighting, camera controls, and click-to-select interaction.

By the end of this project, you'll have:

  • A 3D scene with a glowing sun and multiple orbiting planets, each with unique sizes, colors, and speeds.
  • Interactive camera controls that let you drag to rotate, scroll to zoom, and right-click to pan around the scene.
  • Click detection using raycasting that identifies a planet and displays its name on screen.
  • Secret Mission: Add Saturn's rings and a starfield background to make the scene feel like real outer space.

Are there any prerequisites?

Basic familiarity with HTML, CSS, and JavaScript is helpful. No prior 3D or Three.js experience is needed.

Set Up Your Development Environment

Every 3D web project starts the same way: getting the right tools installed and connected. Three.js is a JavaScript library, so it runs on Node.js and uses npm to manage packages.

In this step, you'll install Node.js, create a project folder, and add Three.js and Vite as your core dependencies. By the end, you'll have a working development server and a verified setup.

In this step, get ready to:
  • Install Node.js and create a project folder.
  • Install Three.js and Vite as project dependencies.
  • Set up your HTML and JavaScript files, then verify everything runs.
Install Node.js and create the project folder

Node.js lets you run JavaScript outside the browser. It also gives you npm, the package manager you'll use to install Three.js and Vite.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Check if Node.js is already installed by running this command:
node --version

✔️ I see a version number

Node.js is ready to go. Move on to the next instruction.

ⓧ Command not found

  • Go to nodejs.org and download the LTS version.
  • Run the installer and follow the prompts to completion.
  • Close and reopen Terminal after the install finishes.
  • Run node --version again to confirm it works.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and initialize it by running these commands:
mkdir 3d-solar-system
cd 3d-solar-system
npm init -y

What do these commands do?

  • mkdir 3d-solar-system creates a new folder called 3d-solar-system.
  • cd 3d-solar-system moves you into that folder.
  • npm init -y creates a package.json file that tracks your project's name, version, and dependencies. The -y flag accepts all the default settings.
  • Confirm the file was created by running:
ls

You should see package.json in the output.

Don't see package.json?

  • Make sure you ran npm init -y from inside the 3d-solar-system folder.
  • Run pwd to check your current location. It should end with 3d-solar-system.
  • I ran npm init -y but don't see package.json in my folder. How do I fix this?
Install Three.js and Vite

Three.js is the library that creates 3D graphics in your browser. Vite is a build tool that runs a local development server and handles module imports for you.

  • Install Three.js as a project dependency by running:
npm install --save three@0.184.0
  • Install Vite as a development dependency by running:
npm install --save-dev vite@8.0.3

Both commands should show a confirmation message with the number of packages added. No error messages should appear.

Why --save vs --save-dev?

The --save flag marks Three.js as a production dependency because your app needs it to run. The --save-dev flag marks Vite as a development-only tool because it only runs your local server during development.

Seeing errors during install?

  • Make sure you have an active internet connection.
  • Check that you are inside the 3d-solar-system folder by running pwd.
  • I'm getting errors when running npm install for Three.js or Vite. How do I fix this?
  • Create the two files your project needs by running:

macOS

touch index.html main.js

Windows

New-Item index.html, main.js
  • Confirm the files exist by running:
ls

You should see index.html, main.js, node_modules, package-lock.json, and package.json in the output.

Set up the HTML file and start the dev server

Your files are empty right now. You need to add the HTML structure that creates a full-screen canvas and the JavaScript that imports Three.js.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the 3d-solar-system folder on your Desktop, then click Open.
  • In the Explorer sidebar on the left, click index.html to open it.
  • Copy the following code into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>3D Solar System</title>
  <style>
    /* Reset default browser spacing */
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    /* Full-screen black background with no scrollbars */
    body {
      overflow: hidden;
      background: #000;
    }
    /* Ensure the Three.js canvas fills its container */
    canvas {
      display: block;
    }
  </style>
</head>
<body>
  <!-- Load main.js as an ES module so we can use import statements -->
  <script type="module" src="/main.js"></script>
</body>
</html>

What does this HTML do?

  • The CSS reset (* { margin: 0; ... }) removes default browser spacing so the Three.js canvas can fill the entire screen.
  • overflow: hidden on the body prevents scrollbars from appearing.
  • The <script type="module"> tag loads main.js as an ES module, which lets you use import statements in your JavaScript.
  • Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).

You won't see a change in the browser yet. The main.js file will bring this page to life in the next chunk.

✔️ Awesome, I've got everything!

Double check you've saved index.html.

ⓧ I'd like to double check the full code

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>3D Solar System</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    body {
      overflow: hidden;
      background: #000;
    }
    canvas {
      display: block;
    }
  </style>
</head>
<body>
  <script type="module" src="/main.js"></script>
</body>
</html>
  • In the Explorer sidebar, click main.js to open it.
  • Copy the following code into main.js:
// Import the entire Three.js library
import * as THREE from 'three';

// Verify the setup works by printing to the browser console
console.log('Three.js is ready!');

What does this code do?

  • import * as THREE from 'three' pulls in the entire Three.js library and makes it available under the name THREE.
  • The console.log prints a message to your browser's developer console so you can confirm the import is working.
  • Save main.js by pressing Cmd+S (macOS) or Ctrl+S (Windows).

✔️ Awesome, I've got everything!

Double check you've saved main.js.

ⓧ I'd like to double check the full code

import * as THREE from 'three';

console.log('Three.js is ready!');
  • Switch back to your Terminal.
  • Start the Vite development server by running:
npx vite

What is Vite doing?

Vite starts a local development server at http://localhost:5173. It serves your project files and automatically refreshes the browser when you save changes.

  • Open your browser and go to http://localhost:5173.

You should see a black screen. This is expected because you have not added any 3D content yet.

  • Open the browser console by pressing Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows).

You should see the message Three.js is ready! in the console. That confirms Three.js is installed and your files are connected.

Not seeing 'Three.js is ready!' in the console?

  • Make sure the Vite dev server is still running in Terminal. You should see a message with Local: http://localhost:5173/.
  • Check that main.js is saved and contains the import and console.log lines.
  • Make sure index.html has <script type="module" src="/main.js"></script> inside the <body> tag.
  • I opened localhost:5173 but don't see 'Three.js is ready!' in my console. How do I debug this?

Your development environment is ready to go. Next up, you'll create the 3D scene and add a glowing sun at the center.

Create the 3D Scene with a Sun

Your environment is set up and Three.js is loaded. Now it's time to build the actual 3D scene.

Every Three.js project needs three core building blocks: a Scene to hold your objects, a Camera to define the viewpoint, and a Renderer to draw everything to the screen. In this step, you'll set up all three and add a glowing sun at the center.

In this step, get ready to:
  • Initialize the core Three.js objects: Scene, Camera, and Renderer.
  • Add a sun and lighting to the scene.
  • Create an animation loop to bring the scene to life.
Initialize the scene, camera, and renderer

Before drawing anything in 3D, you need to prepare both the HTML page and the JavaScript foundation. The HTML page needs an overlay element where you'll later display planet names.

The JavaScript file needs the three core objects that every Three.js project depends on: a Scene, a Camera, and a Renderer.

  • Open index.html in VS Code and replace its entire contents with the following code:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>3D Solar System</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    body {
      overflow: hidden;
      background: #000;
    }
    canvas {
      display: block;
    }
    /* Info overlay for displaying planet names */
    #info {
      position: absolute;
      top: 20px;
      left: 20px;
      color: #fff;
      font-family: Arial, sans-serif;
      font-size: 24px;
      pointer-events: none;
      text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
    }
  </style>
</head>
<body>
  <!-- Overlay text that shows the selected planet's name -->
  <div id="info">Click a planet to see its name</div>
  <script type="module" src="/main.js"></script>
</body>
</html>

What changed in the HTML?

The #info styles create a white text overlay pinned to the top-left corner of the page. pointer-events: none ensures the overlay doesn't block clicks on the 3D canvas behind it.

The <div id="info"> element will display planet names when you click on them in a later step.

  • Save index.html.
  • Check your browser. You should see white text reading "Click a planet to see its name" on a black background.

Don't see the text?

  • Make sure you saved the file. Vite auto-reloads the browser on save.
  • Check that the <div id="info"> is inside the <body> tag, before the <script> tag.
  • Make sure Vite is still running in your terminal. If it stopped, restart it by running npx vite from your project folder.

Still stuck? Ask for help.

Now it's time to set up the Three.js foundation. The Scene is a container for all your 3D objects. The Camera determines what part of the scene is visible. The Renderer takes the scene and camera and draws the result to an HTML canvas.

  • Open main.js and replace its entire contents with the following code:
import * as THREE from 'three';

// Scene setup
const scene = new THREE.Scene();

// Camera: defines the viewpoint into the 3D world
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.z = 50;

// Renderer: draws the scene to an HTML canvas
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);

What does this code do?

  • THREE.Scene() creates an empty container for all your 3D objects and lights.
  • THREE.PerspectiveCamera(75, ...) simulates how a human eye sees the world. 75 is the field of view in degrees. The near (0.1) and far (1000) values define the visible range.
  • camera.position.z = 50 pulls the camera back so you can see objects placed at the center of the scene.
  • THREE.WebGLRenderer({ antialias: true }) creates the renderer that draws to an HTML canvas. Antialiasing smooths jagged edges on your 3D shapes.
  • setPixelRatio(Math.min(window.devicePixelRatio, 2)) keeps the canvas sharp on high-DPI screens without wasting GPU power.
  • Save main.js.
  • You won't see a visible change in the browser yet. The scene exists in memory, but nothing is being drawn to the screen.

Seeing an error in the browser console?

  • Open the browser console with Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows) and look for red error messages.
  • Check that the import line reads exactly import * as THREE from 'three' with quotes around three.
  • Make sure Vite is still running in your terminal.

Help me debug my Three.js scene setup.

Add the sun and lighting

3D objects are invisible without light. You need light sources in the scene for the renderer to calculate how objects look.

You'll add two types of light and a glowing sun sphere. The sun uses a special material that ignores lighting, so it always appears bright.

  • In main.js, find the line that says document.body.appendChild(renderer.domElement); and add the following code below it:
// Lighting
const pointLight = new THREE.PointLight(0xffffff, 500, 300);
pointLight.position.set(0, 0, 0);
scene.add(pointLight);

const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);

// Sun
const sunGeometry = new THREE.SphereGeometry(5, 32, 32);
const sunMaterial = new THREE.MeshBasicMaterial({ color: 0xffaa00 });
const sun = new THREE.Mesh(sunGeometry, sunMaterial);
scene.add(sun);

What does this code do?

  • THREE.PointLight(0xffffff, 500, 300) creates a white light at the center of the scene that radiates outward in all directions. The intensity of 500 is high enough to illuminate planets at orbital distances.
  • THREE.AmbientLight(0x404040, 0.5) adds a dim background light so the dark sides of planets are still slightly visible instead of pitch black.
  • THREE.SphereGeometry(5, 32, 32) creates a sphere shape with a radius of 5 and 32 segments in each direction for a smooth surface.
  • THREE.MeshBasicMaterial makes the sun appear uniformly bright. Unlike MeshStandardMaterial, it ignores scene lighting entirely. This makes the sun look self-lit.
  • THREE.Mesh combines the geometry (shape) and material (appearance) into a single 3D object you can add to the scene.
  • Save main.js.
  • You won't see the sun in the browser yet. The scene has objects now, but there is no animation loop drawing them to the screen.
Create the animation loop and make the scene responsive

You have a scene with a sun and lights, but nothing is being drawn to the screen. The renderer needs to be told to draw each frame.

You also need the canvas to resize when the browser window changes size. Without this, the scene would stretch or squash if the window shape changed.

  • In main.js, find the line that says scene.add(sun); and add the following code below it:
// Responsive canvas
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

// Animation loop
function animate() {
  requestAnimationFrame(animate);

  // Rotate the sun
  sun.rotation.y += 0.005;

  renderer.render(scene, camera);
}

animate();

What does this code do?

  • The resize event listener updates the camera's aspect ratio and renderer size whenever the browser window changes. camera.updateProjectionMatrix() applies the new aspect ratio so the scene doesn't stretch.
  • requestAnimationFrame(animate) creates a loop that runs about 60 times per second. Each frame, it draws the entire scene to the canvas.
  • sun.rotation.y += 0.005 slowly spins the sun on its vertical axis each frame for a subtle visual effect.
  • renderer.render(scene, camera) is the line that actually draws the scene to the canvas. Without it, nothing appears.
  • The final animate() call at the bottom kicks off the loop for the first time.
  • Save main.js.
  • Check your browser. You should see a glowing yellow-orange sphere on a black background, slowly rotating.

Don't see the sun?

  • Make sure you saved both index.html and main.js. Unsaved changes won't appear in the browser.
  • Open the browser console with Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows) and look for any red error messages.
  • Check that animate(); is called at the very bottom of main.js, outside the function definition. Without this call, the animation loop never starts.

Help me debug my Three.js sun.

Double check that your code matches the full files below.

✔️ Awesome, I've got everything!

Make sure you've saved both index.html and main.js.

ⓧ I'd like to double check the full code

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>3D Solar System</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    body {
      overflow: hidden;
      background: #000;
    }
    canvas {
      display: block;
    }
    #info {
      position: absolute;
      top: 20px;
      left: 20px;
      color: #fff;
      font-family: Arial, sans-serif;
      font-size: 24px;
      pointer-events: none;
      text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
    }
  </style>
</head>
<body>
  <div id="info">Click a planet to see its name</div>
  <script type="module" src="/main.js"></script>
</body>
</html>
import * as THREE from 'three';

// Scene setup
const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.z = 50;

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);

// Lighting
const pointLight = new THREE.PointLight(0xffffff, 500, 300);
pointLight.position.set(0, 0, 0);
scene.add(pointLight);

const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);

// Sun
const sunGeometry = new THREE.SphereGeometry(5, 32, 32);
const sunMaterial = new THREE.MeshBasicMaterial({ color: 0xffaa00 });
const sun = new THREE.Mesh(sunGeometry, sunMaterial);
scene.add(sun);

// Responsive canvas
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

// Animation loop
function animate() {
  requestAnimationFrame(animate);

  // Rotate the sun
  sun.rotation.y += 0.005;

  renderer.render(scene, camera);
}

animate();

Your sun is shining at the center of the scene. Next up, you'll add colorful planets that orbit around it at different speeds.

Add Orbiting Planets

Your sun is glowing at the center of the scene. A solar system needs more than a star, though.

In this step, you will create seven planets that orbit the sun at different speeds and distances. You will write a factory function that stamps out planet objects, then bring them to life with trigonometry inside the animation loop.

In this step, get ready to:
  • Build a reusable planet factory function.
  • Create seven planets with unique sizes, colors, and orbital speeds.
  • Animate each planet to orbit the sun using trigonometry.
Build a planet factory function

Every planet needs the same set of parts: a sphere geometry, a material, a mesh, and orbital properties like distance and speed. Instead of repeating that setup seven times, a single factory function can stamp out each planet from a set of parameters.

  • In main.js, find the line that says scene.add(sun);.
  • Add the following createPlanet function below it, before the // Responsive canvas section:
// Planet factory
function createPlanet(name, radius, color, distance, speed) {
  const geometry = new THREE.SphereGeometry(radius, 32, 32);
  const material = new THREE.MeshStandardMaterial({ color: color });
  const mesh = new THREE.Mesh(geometry, material);
  mesh.position.x = distance;

  return {
    name: name,
    mesh: mesh,
    distance: distance,
    angle: Math.random() * Math.PI * 2,
    speed: speed,
  };
}

What does this code do?

The function takes five parameters: a name, a sphere radius, a color, an orbit distance from the sun, and an orbital speed.

  • THREE.MeshStandardMaterial is used instead of MeshBasicMaterial. Standard materials respond to lighting, so planets get realistic bright and dark sides as they face toward or away from the sun.
  • mesh.position.x = distance places the planet at its starting orbit distance along the x-axis.
  • angle starts at a random value between 0 and 2π, so planets do not all line up when the scene first loads.

You will not see a change in the browser yet. The next section uses this function to create the planets.

Create the planets and add them to the scene

Now you can call createPlanet once for each planet, giving each one a unique size, color, distance, and speed. After creating them, a loop adds every planet mesh to the scene so Three.js knows to render them.

  • Directly below the createPlanet function you just added, copy the following code:
// Create planets
const planets = [
  createPlanet('Mercury', 0.8, 0xaaaaaa, 10, 0.02),
  createPlanet('Venus', 1.2, 0xffcc66, 14, 0.015),
  createPlanet('Earth', 1.3, 0x4488ff, 18, 0.012),
  createPlanet('Mars', 1.0, 0xff4422, 22, 0.01),
  createPlanet('Jupiter', 3.0, 0xff8844, 30, 0.006),
  createPlanet('Saturn', 2.5, 0xffcc88, 38, 0.004),
  createPlanet('Neptune', 1.8, 0x4466ff, 46, 0.002),
];

// Add planet meshes to scene
const planetMeshes = [];
for (const planet of planets) {
  scene.add(planet.mesh);
  planetMeshes.push(planet.mesh);
}

What does this code do?

The planets array stores seven planet objects. Each call to createPlanet passes a different name, radius, color, orbit distance, and speed.

  • Smaller planets like Mercury have a radius of 0.8 and orbit close to the sun (distance 10). Larger planets like Jupiter have a radius of 3.0 and sit farther out (distance 30).
  • The planetMeshes array collects just the mesh objects. You will use this later for click detection, where you need a flat list of meshes to test against.
  • Save main.js.
  • Check your browser. You should see several colored spheres arranged along a line extending from the sun. They are not orbiting yet.

Don't see any planets?

  • Make sure you saved main.js. Vite auto-refreshes only after the file is saved.
  • Check that the new code sits between scene.add(sun); and the // Responsive canvas section. Placing it inside the animate function would recreate planets every frame.
  • Open your browser console (Cmd+Option+J on macOS or Ctrl+Shift+J on Windows) and look for any red error messages.
  • Still stuck? Help me figure out why my planets are not showing up in the Three.js scene.
Animate the orbits

The planets are in the scene, but they are frozen. To make each planet orbit the sun, you need to update its position every frame using a bit of trigonometry.

A circle can be traced with sine and cosine. If you feed a growing angle into Math.cos for x and Math.sin for z, the planet moves in a smooth circle around the origin.

  • Inside the animate function in main.js, find this line:
  sun.rotation.y += 0.005;
  • Add the following orbit loop directly below that line, before renderer.render(scene, camera);:
  // Orbit the planets
  for (const planet of planets) {
    planet.angle += planet.speed;
    planet.mesh.position.x = Math.cos(planet.angle) * planet.distance;
    planet.mesh.position.z = Math.sin(planet.angle) * planet.distance;
  }

What does this code do?

  • planet.angle += planet.speed increases the angle a tiny amount each frame. A higher speed value means the planet completes its orbit faster.
  • Math.cos(angle) * distance sets the x position and Math.sin(angle) * distance sets the z position. Together, they trace a circle in the xz plane (the "ground" of your 3D world).
  • Inner planets like Mercury have a speed of 0.02 and zip around quickly. Outer planets like Neptune have a speed of 0.002 and drift slowly, just like in real space.
  • Save main.js.
  • Check your browser. You should see colorful planets orbiting the sun at different speeds. Mercury zips around close to the sun while Neptune crawls along the outer edge.

Planets not moving?

  • Make sure the orbit loop is inside the animate function, not outside it. Code outside animate only runs once, so the planets would never update their positions.
  • Check that the loop uses planet.angle and planet.distance, not angle or distance on their own. Each property belongs to the planet object.
  • Still stuck? Help me figure out why my planets are not orbiting the sun.

✔️ Awesome, I've got everything!

Great. Double check you have saved main.js before moving on.

ⓧ I'd like to double check the full code

Here is the complete main.js file at this point. Compare it with yours and fix any differences.

import * as THREE from 'three';

// Scene setup
const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.z = 50;

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);

// Lighting
const pointLight = new THREE.PointLight(0xffffff, 500, 300);
pointLight.position.set(0, 0, 0);
scene.add(pointLight);

const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);

// Sun
const sunGeometry = new THREE.SphereGeometry(5, 32, 32);
const sunMaterial = new THREE.MeshBasicMaterial({ color: 0xffaa00 });
const sun = new THREE.Mesh(sunGeometry, sunMaterial);
scene.add(sun);

// Planet factory
function createPlanet(name, radius, color, distance, speed) {
  const geometry = new THREE.SphereGeometry(radius, 32, 32);
  const material = new THREE.MeshStandardMaterial({ color: color });
  const mesh = new THREE.Mesh(geometry, material);
  mesh.position.x = distance;

  return {
    name: name,
    mesh: mesh,
    distance: distance,
    angle: Math.random() * Math.PI * 2,
    speed: speed,
  };
}

// Create planets
const planets = [
  createPlanet('Mercury', 0.8, 0xaaaaaa, 10, 0.02),
  createPlanet('Venus', 1.2, 0xffcc66, 14, 0.015),
  createPlanet('Earth', 1.3, 0x4488ff, 18, 0.012),
  createPlanet('Mars', 1.0, 0xff4422, 22, 0.01),
  createPlanet('Jupiter', 3.0, 0xff8844, 30, 0.006),
  createPlanet('Saturn', 2.5, 0xffcc88, 38, 0.004),
  createPlanet('Neptune', 1.8, 0x4466ff, 46, 0.002),
];

// Add planet meshes to scene
const planetMeshes = [];
for (const planet of planets) {
  scene.add(planet.mesh);
  planetMeshes.push(planet.mesh);
}

// Responsive canvas
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

// Animation loop
function animate() {
  requestAnimationFrame(animate);

  // Rotate the sun
  sun.rotation.y += 0.005;

  // Orbit the planets
  for (const planet of planets) {
    planet.angle += planet.speed;
    planet.mesh.position.x = Math.cos(planet.angle) * planet.distance;
    planet.mesh.position.z = Math.sin(planet.angle) * planet.distance;
  }

  renderer.render(scene, camera);
}

animate();

Your solar system is alive. Planets are circling the sun and the scene already feels dynamic. Next up, you will add camera controls so you can fly around the scene and click on any planet to learn its name.

Add Camera Controls and Click Interaction

Your planets are orbiting the sun in a colorful 3D scene. Right now the camera is stuck at a fixed angle.

Three.js includes an addon called OrbitControls that lets users drag, zoom, and pan around a 3D scene. Combined with a technique called raycasting, you can also detect which 3D object the user clicks on.

In this step, get ready to:
  • Add camera controls for dragging, zooming, and panning.
  • Detect clicks on planets using raycasting.
  • Display the clicked planet's name on screen.
Add OrbitControls for camera movement

OrbitControls is a Three.js addon that gives you full camera movement with just a few lines of code. You'll be able to drag to rotate, scroll to zoom, and right-click to pan.

  • In main.js, find the import at the very top of the file:
import * as THREE from 'three';
  • Add a new import for OrbitControls directly below it, so the top of your file looks like this:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
  • Now scroll down to the for loop that adds planet meshes to the scene. Add this code directly below it:
// OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

What does this code do?

new OrbitControls(camera, renderer.domElement) creates a controls instance that listens for mouse and touch events on the canvas. It automatically moves the camera when the user drags, scrolls, or right-clicks.

enableDamping = true adds a smooth, inertial feel to the camera movement instead of stopping abruptly when you release the mouse.

For damping to work, OrbitControls needs to be updated every frame inside the animation loop.

  • Inside the animate function, find this line:
  renderer.render(scene, camera);
  • Add controls.update(); directly above it, so it looks like this:
  controls.update();
  renderer.render(scene, camera);
  • Save main.js.
  • In your browser, try dragging to rotate the view, scrolling to zoom in and out, and right-clicking (or two-finger dragging on a trackpad) to pan. The camera should move smoothly around the solar system.

Camera not moving when you drag?

  • Make sure the import path is exactly three/addons/controls/OrbitControls.js. A typo in the path causes a silent import failure.
  • Check that controls.update() is inside the animate function, not outside it. Without this call, damping and camera updates do not take effect.
  • Open the browser console (**Cmd+Option+J** on macOS, **Ctrl+Shift+J** on Windows) and check for any red error messages.

Help me debug my OrbitControls setup

Set up raycasting for click detection

Raycasting shoots an invisible ray from the camera through the point where the user clicked. If that ray hits a 3D object, you know the user clicked on it.

Your index.html already has a #info div styled as a white text overlay in the top-left corner. You just need to connect it to your JavaScript so you can update it when a planet is clicked.

  • In main.js, add this code directly below the OrbitControls section you just wrote:
// Raycasting for click detection
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const infoDiv = document.getElementById('info');

What does this code do?

  • raycaster is the object that fires an invisible ray into the scene to check what it hits.
  • mouse stores the click position as a 2D coordinate that the raycaster can use.
  • infoDiv is a reference to the HTML overlay where you will display the planet name.

You will not see any visible change yet. The next chunk wires up the actual click detection.

Detect planet clicks and display the name

Now you need a click event listener that converts the mouse position into coordinates the raycaster understands, fires the ray, and checks if it hits any planet.

  • Still in main.js, add this click event listener directly below the infoDiv line:
renderer.domElement.addEventListener('click', (event) => {
  // Convert mouse position to normalized device coordinates (-1 to 1)
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

  // Fire a ray from the camera through the click point
  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObjects(planetMeshes);

  // If the ray hit a planet, display its name
  if (intersects.length > 0) {
    const clickedMesh = intersects[0].object;
    const planet = planets.find((p) => p.mesh === clickedMesh);
    if (planet) {
      infoDiv.textContent = planet.name;
    }
  }
});

What does this code do?

The raycaster needs mouse coordinates in a special format called normalized device coordinates, where both x and y range from -1 to 1. The two formulas convert regular pixel positions into this range.

raycaster.setFromCamera(mouse, camera) fires an invisible ray from the camera through the click point. raycaster.intersectObjects(planetMeshes) checks if that ray hits any of the planet meshes and returns the results sorted by distance, closest first.

If there is a hit, intersects[0].object gives you the closest mesh. The code then looks up the matching planet object in the planets array and updates the overlay text with its name.

  • Save main.js.
  • In your browser, click on any planet. The text in the top-left corner should change from "Click a planet to see its name" to that planet's name (for example, "Jupiter" or "Earth").

Clicking a planet does nothing?

  • Small planets like Mercury are hard to click. Try clicking on Jupiter first since it is the largest.
  • Make sure you passed planetMeshes (the array of mesh objects) to intersectObjects, not planets (the array of planet data objects). The raycaster only works with Three.js meshes.
  • Check that the y formula has a minus sign at the front: -(event.clientY / window.innerHeight) * 2 + 1. The browser's y-axis is flipped compared to Three.js, so missing the minus sign causes the ray to fire in the wrong direction.

Help me debug my raycasting click detection

Why only check planetMeshes?

The scene also contains the sun, lights, and the camera itself. Passing only planetMeshes to intersectObjects ensures the raycaster ignores everything except the planets you want to be clickable.

✔️ Awesome, I've got everything!

Great. Double check you have saved main.js before continuing.

ⓧ I'd like to double check the full code

Here is the complete main.js at this point. Compare it with yours and fix any differences.

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// Scene setup
const scene = new THREE.Scene();

const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.z = 50;

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);

// Lighting
const pointLight = new THREE.PointLight(0xffffff, 500, 300);
pointLight.position.set(0, 0, 0);
scene.add(pointLight);

const ambientLight = new THREE.AmbientLight(0x404040, 0.5);
scene.add(ambientLight);

// Sun
const sunGeometry = new THREE.SphereGeometry(5, 32, 32);
const sunMaterial = new THREE.MeshBasicMaterial({ color: 0xffaa00 });
const sun = new THREE.Mesh(sunGeometry, sunMaterial);
scene.add(sun);

// Planet factory
function createPlanet(name, radius, color, distance, speed) {
  const geometry = new THREE.SphereGeometry(radius, 32, 32);
  const material = new THREE.MeshStandardMaterial({ color: color });
  const mesh = new THREE.Mesh(geometry, material);
  mesh.position.x = distance;

  return {
    name: name,
    mesh: mesh,
    distance: distance,
    angle: Math.random() * Math.PI * 2,
    speed: speed,
  };
}

// Create planets
const planets = [
  createPlanet('Mercury', 0.8, 0xaaaaaa, 10, 0.02),
  createPlanet('Venus', 1.2, 0xffcc66, 14, 0.015),
  createPlanet('Earth', 1.3, 0x4488ff, 18, 0.012),
  createPlanet('Mars', 1.0, 0xff4422, 22, 0.01),
  createPlanet('Jupiter', 3.0, 0xff8844, 30, 0.006),
  createPlanet('Saturn', 2.5, 0xffcc88, 38, 0.004),
  createPlanet('Neptune', 1.8, 0x4466ff, 46, 0.002),
];

// Add planet meshes to scene
const planetMeshes = [];
for (const planet of planets) {
  scene.add(planet.mesh);
  planetMeshes.push(planet.mesh);
}

// OrbitControls
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// Raycasting for click detection
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const infoDiv = document.getElementById('info');

renderer.domElement.addEventListener('click', (event) => {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

  raycaster.setFromCamera(mouse, camera);
  const intersects = raycaster.intersectObjects(planetMeshes);

  if (intersects.length > 0) {
    const clickedMesh = intersects[0].object;
    const planet = planets.find((p) => p.mesh === clickedMesh);
    if (planet) {
      infoDiv.textContent = planet.name;
    }
  }
});

// Responsive canvas
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

// Animation loop
function animate() {
  requestAnimationFrame(animate);

  // Rotate the sun
  sun.rotation.y += 0.005;

  // Orbit the planets
  for (const planet of planets) {
    planet.angle += planet.speed;
    planet.mesh.position.x = Math.cos(planet.angle) * planet.distance;
    planet.mesh.position.z = Math.sin(planet.angle) * planet.distance;
  }

  controls.update();
  renderer.render(scene, camera);
}

animate();

Secret mission

Add Saturn's Rings and a Starfield

Your solar system looks great, but real space is more dramatic. In this mission, you will add a tilted ring around Saturn and scatter a thousand stars across the background to make the scene feel like actual outer space.

Before you go

Clean Up Your Resources

This project runs entirely on your local machine, so there are no ongoing cloud costs. Decide whether to keep your project files as they are, pause by stopping the dev server, or delete everything entirely.

Resources you used:

  • The 3d-solar-system project folder on your Desktop.
  • The node_modules folder containing Three.js and Vite packages.
  • The index.html and main.js files you created.
  • Node.js installed on your system.
  • The Vite dev server (if still running in your terminal).

Keep everything running

No action needed. Choose this if you want to keep experimenting with your solar system or plan to extend it further.

  • Your project files stay on your Desktop in the 3d-solar-system folder.
  • If the Vite dev server is still running in your terminal, you can keep it going or restart it anytime with npx vite.

Pause - I'll come back to this later

Stop the dev server to free up your terminal. Your files stay safe on disk.

  • In the terminal where Vite is running, press Ctrl+C to stop the dev server.
  • When you are ready to come back, open your terminal, navigate to the 3d-solar-system folder, and run npx vite to start the server again.

Delete - I don't want to use this again

Remove all project files from your machine. Node.js stays installed since you may need it for other projects.

  • In the terminal where Vite is running, press Ctrl+C to stop the dev server.
  • Delete the entire 3d-solar-system folder from your Desktop.
  • If you want to uninstall Node.js as well, open Finder (macOS) or Settings (Windows) and remove it from your installed applications.

Mission accomplished

That's a Wrap!

Nice work! You just built a fully interactive 3D solar system in the browser using Three.js. Planets orbit a glowing sun, you can fly around the scene with your mouse, and clicking any planet displays its name on screen.

You've learned how to:

  • Build a 3D scene from scratch with a Scene, PerspectiveCamera, and WebGLRenderer, then animate orbiting planets using trigonometry and requestAnimationFrame.
  • Add interactive camera controls with OrbitControls so the user can drag, zoom, and pan around the 3D scene.
  • Detect mouse clicks on 3D objects using a Raycaster to identify which planet the user selected and display its name on screen.
  • Secret Mission: Added Saturn's rings with RingGeometry and a starfield background with BufferGeometry and Points to make the scene feel like real outer space.

Ready to quiz yourself?