Tutorial: Build a Solar System VR in A-Frame
Step 1 – Set Up Your Project
- Create a folder called
SolarSystemVR
- Inside it, create a file named
index.html
- Create another folder inside called
images
. Put planet textures inside (sun, earth, mars, etc.), plus a galaxy background.
├── index.html
└── images/
├── sun.jpeg
├── earth.jpeg
├── mars.jpeg
├── mercury.jpeg
├── jupiter.jpeg
├── saturn.jpg
├── uranus.jpeg
├── neptune.jpg
├── venus.jpeg
└── milky.jpg
Step 2 – Add the A-Frame Base
Open index.html
and paste:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Solar System VR</title>
<!-- Load A-Frame -->
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
</head>
<body>
<a-scene background="src: #sky"
cursor="rayOrigin: mouse"
raycaster="objects: .clickable; far: 50">
<!-- We will add content here -->
</a-scene>
</body>
</html>
Step 3 – Add Assets (Planet Textures)
Inside <a-scene>
, add:
<a-assets>
<img id="sun" src="images/sun.jpeg">
<img id="earth" src="images/earth.jpeg">
<img id="mars" src="images/mars.jpeg">
<img id="mercury" src="images/mercury.jpeg">
<img id="jupiter" src="images/jupiter.jpeg">
<img id="saturn" src="images/saturn.jpg">
<img id="uranus" src="images/uranus.jpeg">
<img id="neptune" src="images/neptune.jpg">
<img id="venus" src="images/venus.jpeg">
<img id="sky" src="images/milky.jpg">
</a-assets>
Step 4 – Add the Camera Rig
This lets the user look and move:
<!-- Camera rig -->
<a-entity id="rig" position="0 1.6 0"
animation__move="property: position; dur: 2000; easing: easeInOutQuad">
<!-- Main camera -->
<a-entity id="camera" camera look-controls wasd-controls position="0 0 0"></a-entity>
<!-- VR controllers (for Oculus Quest etc.) -->
<a-entity id="rightHand"
laser-controls="hand: right"
cursor="rayOrigin: entity"
raycaster="objects: .clickable; far: 50"
line="opacity: 0.9"></a-entity>
<a-entity id="leftHand"
laser-controls="hand: left"
cursor="rayOrigin: entity"
raycaster="objects: .clickable; far: 50"
line="opacity: 0.9"></a-entity>
</a-entity>
Step 5 – Add Info Panel and Buttons
We need a popup to show facts + buttons to close or reset.
<!-- Info panel -->
<a-entity id="infoPanel" visible="false" position="0 0 -1.2"
geometry="primitive: plane; width: 1.2; height: 0.6"
material="color: #222; opacity: 0.9">
<a-text id="infoText" value="Info" align="center" width="1.1" wrap-count="28"
position="0 0.06 0.01"></a-text>
</a-entity>
<!-- Close button -->
<a-entity position="0 -0.22 -1.2">
<a-plane class="clickable" width="0.35" height="0.12"
material="color: #f44336; opacity: 0.95"
onclick="document.querySelector('#infoPanel').setAttribute('visible', false)"></a-plane>
<a-text value="Close" align="center" position="0 0 0.01" width="0.9"></a-text>
</a-entity>
<!-- Return to Start button -->
<a-entity position="0 -0.5 -1.2">
<a-plane class="clickable" width="0.6" height="0.15" color="#2196f3" opacity="0.95"
onclick="document.querySelector('#rig').setAttribute('animation__move',
'property: position; to: 0 1.6 0; dur: 2000; easing: easeInOutQuad')"></a-plane>
<a-text value="Return to Start" align="center" position="0 0 0.01" width="0.9" color="#fff"></a-text>
</a-entity>
Step 6 – Add the Planets
Each planet is an <a-sphere>
with a texture and click event.
Example: Earth 🌍
<a-sphere class="clickable" src="#earth" position="-3 3 -10" radius="0.5"
animation="property: rotation; to: 0 360 0; loop: true; dur: 80000; easing: linear"
onclick="
document.querySelector('#rig').setAttribute(
'animation__move','property: position; to: -3 2 -8; dur: 2000; easing: easeInOutQuad'
);
document.querySelector('#infoText').setAttribute('value','EARTH — Our home — Year: 365 days');
document.querySelector('#infoPanel').setAttribute('visible', true);
">
</a-sphere>
Example: Sun ☀️
<a-sphere class="clickable" src="#sun" position="-13 2 -10" radius="4"
onclick="
document.querySelector('#rig').setAttribute(
'animation__move','property: position; to: -8 2 -8; dur: 2000; easing: easeInOutQuad'
);
document.querySelector('#infoText').setAttribute('value','SUN — G2V yellow dwarf — ~5,778 K');
document.querySelector('#infoPanel').setAttribute('visible', true);
">
</a-sphere>
Do the same for Mercury, Mars, Jupiter, Saturn, Uranus, Neptune, and Venus (with their own size, position, and info text).
Step 7 – Save and Test
- Save the file
- Open it in your browser
- Try clicking planets → you should zoom in and see facts
- Use "Close" to hide panel, "Return to Start" to reset
🎯 Reflection
- How does the rig help with camera movement?
- Why is onclick used for planets?
- How could you add Saturn's rings?
🚀 Extension Challenges
- Add rotation animations to all planets
- Add a new button: "Go to Sun"
- Add fun facts for each planet
- Test your project in a VR headset
📄 Final Full Code
Here's the complete index.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Solar System VR</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
</head>
<body>
<a-scene background="src: #sky"
cursor="rayOrigin: mouse"
raycaster="objects: .clickable; far: 50">
<a-assets>
<img id="sun" src="images/sun.jpeg">
<img id="earth" src="images/earth.jpeg">
<img id="jupiter" src="images/jupiter.jpeg">
<img id="mars" src="images/mars.jpeg">
<img id="mercury" src="images/mercury.jpeg">
<img id="neptune" src="images/neptune.jpg">
<img id="saturn" src="images/saturn.jpg">
<img id="uranus" src="images/uranus.jpeg">
<img id="venus" src="images/venus.jpeg">
<img id="sky" src="images/milky.jpg">
</a-assets>
<!-- Camera rig -->
<a-entity id="rig" position="0 1.6 0"
animation__move="property: position; dur: 2000; easing: easeInOutQuad">
<a-entity id="camera" camera look-controls wasd-controls position="0 0 0"></a-entity>
<a-entity id="rightHand"
laser-controls="hand: right"
cursor="rayOrigin: entity"
raycaster="objects: .clickable; far: 50"
line="opacity: 0.9"></a-entity>
<a-entity id="leftHand"
laser-controls="hand: left"
cursor="rayOrigin: entity"
raycaster="objects: .clickable; far: 50"
line="opacity: 0.9"></a-entity>
<!-- Info panel -->
<a-entity id="infoPanel" visible="false" position="0 0 -1.2"
geometry="primitive: plane; width: 1.2; height: 0.6"
material="color: #222; opacity: 0.9">
<a-text id="infoText" value="Info" align="center" width="1.1" wrap-count="28"
position="0 0.06 0.01"></a-text>
</a-entity>
<!-- Close button -->
<a-entity position="0 -0.22 -1.2">
<a-plane class="clickable" width="0.35" height="0.12"
material="color: #f44336; opacity: 0.95"
onclick="document.querySelector('#infoPanel').setAttribute('visible', false)"></a-plane>
<a-text value="Close" align="center" position="0 0 0.01" width="0.9"></a-text>
</a-entity>
<!-- Return to Start button -->
<a-entity position="0 -0.5 -1.2">
<a-plane class="clickable" width="0.6" height="0.15" color="#2196f3" opacity="0.95"
onclick="document.querySelector('#rig').setAttribute('animation__move',
'property: position; to: 0 1.6 0; dur: 2000; easing: easeInOutQuad')"></a-plane>
<a-text value="Return to Start" align="center" position="0 0 0.01" width="0.9" color="#fff"></a-text>
</a-entity>
</a-entity>
<!-- Planets -->
<a-sphere class="clickable" src="#sun" position="-13 2 -10" radius="4"
onclick="
document.querySelector('#rig').setAttribute(
'animation__move','property: position; to: -8 2 -8; dur: 2000; easing: easeInOutQuad'
);
document.querySelector('#infoText').setAttribute('value','SUN — G2V yellow dwarf — ~5,778 K');
document.querySelector('#infoPanel').setAttribute('visible', true);
"></a-sphere>
<a-sphere class="clickable" src="#mercury" position="-7 2 -10" radius="0.25"
onclick="
document.querySelector('#rig').setAttribute(
'animation__move','property: position; to: -5 2 -8; dur: 2000; easing: easeInOutQuad'
);
document.querySelector('#infoText').setAttribute('value','MERCURY — Smallest planet — Year: 88 days');
document.querySelector('#infoPanel').setAttribute('visible', true);
"></a-sphere>
<a-sphere class="clickable" src="#earth" position="-3 3 -10" radius="0.5"
animation="property: rotation; to: 0 360 0; loop: true; dur: 80000; easing: linear"
onclick="
document.querySelector('#rig').setAttribute(
'animation__move','property: position; to: -3 2 -8; dur: 2000; easing: easeInOutQuad'
);
document.querySelector('#infoText').setAttribute('value','EARTH — Our home — Year: 365 days');
document.querySelector('#infoPanel').setAttribute('visible', true);
"></a-sphere>
<a-sphere class="clickable" src="#mars" position="-2 2 -10" radius="0.25"
onclick="
document.querySelector('#rig').setAttribute(
'animation__move','property: position; to: -2 2 -8; dur: 2000; easing: easeInOutQuad'
);
document.querySelector('#infoText').setAttribute('value','MARS — The Red Planet — Moons: 2');
document.querySelector('#infoPanel').setAttribute('visible', true);
"></a-sphere>
<a-sphere class="clickable" src="#jupiter" position="1 2 -13" radius="1"
onclick="
document.querySelector('#rig').setAttribute(
'animation__move','property: position; to: -2 2 -8; dur: 2000; easing: easeInOutQuad'
);
document.querySelector('#infoText').setAttribute('value','JUPITER — Gas giant — Moons: 95+');
document.querySelector('#infoPanel').setAttribute('visible', true);
"></a-sphere>
<a-sphere class="clickable" src="#saturn" position="4 2 -10" radius="0.8"
onclick="
document.querySelector('#rig').setAttribute(
'animation__move','property: position; to: 6 2 -8; dur: 2000; easing: easeInOutQuad'
);
document.querySelector('#infoText').setAttribute('value','SATURN — Iconic rings — Moons: 80+');
document.querySelector('#infoPanel').setAttribute('visible', true);
"></a-sphere>
<a-sphere class="clickable" src="#uranus" position="7 2 -10" radius="0.75"
onclick="
document.querySelector('#rig').setAttribute(
'animation__move','property: position; to: 9 2 -8; dur: 2000; easing: easeInOutQuad'
);
document.querySelector('#infoText').setAttribute('value','URANUS — Ice giant — Tilt: 98°');
document.querySelector('#infoPanel').setAttribute('visible', true);
"></a-sphere>
<a-sphere class="clickable" src="#neptune" position="10 2 -10" radius="0.75"
onclick="
document.querySelector('#rig').setAttribute(
'animation__move','property: position; to: 12 2 -8; dur: 2000; easing: easeInOutQuad'
);
document.querySelector('#infoText').setAttribute('value','NEPTUNE — Farthest — Very strong winds');
document.querySelector('#infoPanel').setAttribute('visible', true);
"></a-sphere>
<a-sphere class="clickable" src="#venus" position="-1 2 -8" radius="0.45"
onclick="
document.querySelector('#rig').setAttribute(
'animation__move','property: position; to: -1 2 -6; dur: 2000; easing: easeInOutQuad'
);
document.querySelector('#infoText').setAttribute('value','VENUS — Hottest planet — Dense atmosphere');
document.querySelector('#infoPanel').setAttribute('visible', true);
"></a-sphere>
</a-scene>
</body>
</html>
Note: You can also view the Solar System experience using a VR headset. Please keep in mind that the current data is provided for demonstration purposes only, and not all planets are set to rotate. You are welcome to modify the code as you wish for your exploration and practice.