A-Frame WebVR Gallery — Step-by-Step Tutorial

Step-by-Step Tutorial — Build Your Own Virtual Art Gallery

1) Set up your project (2 min)

  1. Create a folder named webvr-gallery
  2. Inside it, create a subfolder named images
  3. Put 9 images in images/ named 1_img.jpg9_img.jpg (or rename later)

Your structure should look like:

webvr-gallery/
├─ index.html
└─ images/
   ├─ 1_img.jpg … 9_img.jpg

2) Make the HTML file (2 min)

Create index.html (empty file). Open it in VS Code or your editor.

3) Paste the minimal A-Frame skeleton (1 min)

Copy this into index.html (this gives you a working empty scene):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>A-Frame Gallery (No JS)</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <script src="https://aframe.io/releases/1.6.0/aframe.min.js"></script>
  <style>html, body { margin:0; height:100%; background:#0b0b12; }</style>
</head>
<body>
  <a-scene renderer="colorManagement: true" background="color: #101018">
  </a-scene>
</body>
</html>

Open the file in your browser (double-click or use VS Code Live Server). You should see a blank A-Frame canvas.

4) Preload the gallery images (2 min)

Inside <a-scene> ... </a-scene>, add an assets block:

<a-assets timeout="30000">
  <img id="img1" src="images/1_img.jpg">
  <img id="img2" src="images/2_img.jpg">
  <img id="img3" src="images/3_img.jpg">
  <img id="img4" src="images/4_img.jpg">
  <img id="img5" src="images/5_img.jpg">
  <img id="img6" src="images/6_img.jpg">
  <img id="img7" src="images/7_img.jpg">
  <img id="img8" src="images/8_img.jpg">
  <img id="img9" src="images/9_img.jpg">
</a-assets>
Why: <a-assets> preloads textures so they don't pop in late.

5) Add lights (1 min)

Still inside <a-scene> add:

<a-entity light="type: ambient; intensity: 0.55; color: #fff"></a-entity>
<a-entity position="0 3 2" light="type: directional; intensity: 0.9; 
castShadow: true"></a-entity>

6) Build the room (floor + 3 walls) (2 min)

<!-- Floor -->
<a-plane rotation="-90 0 0" width="20" height="20"
  color="#2a2a3a" material="roughness:0.9; metalness:0; repeat:4 4"></a-plane>

<!-- Walls -->
<a-plane position="0 2.5 -6" rotation="0 0 0" width="20" height="5"
  color="#171725"></a-plane>
<a-plane position="-10 2.5 0" rotation="0 90 0" width="12" height="5"
  color="#141422"></a-plane>
<a-plane position="10 2.5 0" rotation="0 -90 0" width="12" height="5"
  color="#141422"></a-plane>
Tip: Y-axis is up, Z goes forward/back, X is left/right.

7) Add camera + movement + cursor (2 min)

<a-text value="WASD/Arrows to move • Hover images"
  position="0 4.6 -4" align="center" color="#eaeaf7" width="6"></a-text>

<a-entity id="rig" position="0 1.6 4">
  <a-camera wasd-controls="acceleration: 20">
    <!-- mouse hover (desktop) + gaze (VR) -->
    <a-cursor fuse="false"
      raycaster="objects: .interactable; rayOrigin: mouse; showLine: false">
    </a-cursor>
  </a-camera>
</a-entity>
Important: The cursor raycasts only against elements with class="interactable".

8) Build one framed image (the template) (3 min)

We'll assemble frame + mat + image + caption in a group and position it on the center wall (z ≈ -6):

<a-entity position="-6 3.6 -5.99">
  <!-- Frame -->
  <a-plane width="2.2" height="1.6" color="#0e0e18"></a-plane>
  
  <!-- Mat -->
  <a-plane width="2.06" height="1.46" color="#e7e7ef" position="0 0 0.005"></a-plane>
  
  <!-- Image (interactive, no JS) -->
  <a-image class="interactable" src="#img1" width="1.9" height="1.2"
    position="0 0 0.01"
    material="shader: standard; roughness:0.7; metalness:0.05"
    animation__enter="property: scale; to: 1.2 1.2 1; startEvents: 
      mouseenter; dur: 180; easing: easeOutQuad"
    animation__leave="property: scale; to: 1 1 1; startEvents: 
      mouseleave; dur: 180; easing: easeOutQuad"
    animation__enter_pos="property: position; to: 0 0 0.06; 
      startEvents: mouseenter; dur: 180; easing: easeOutQuad"
    animation__leave_pos="property: position; to: 0 0 0.01; 
      startEvents: mouseleave; dur: 180; easing: easeOutQuad">
  </a-image>
  
  <!-- Caption -->
  <a-text value="Landscape" color="#cfcfe6" width="2.4" 
    position="0 -1.0 0.02" align="center"></a-text>
</a-entity>

Test: Reload the page. Move close to the frame and hover — the image should scale forward slightly.

9) Duplicate to make a 3×3 grid (5–7 min)

Copy your entire <a-entity>...</a-entity> block 8 times and change:

  • position="X Y Z" (grid coordinates)
  • src="#imgN" (pick the right photo)
  • Optional: change the caption text

Use these suggested positions (on the same wall Z = -5.99):

Row (Y) Left X Center X Right X
3.6 -6 -2 2
2.1 -6 -2 2
0.6 -6 -2 2

Remember to switch each image's src: #img1#img9.

Optional wall accents (purely decorative):

<a-plane position="-9.7 2.5 -2" rotation="0 90 0" width="4" height="3"
  color="#1b1b2b"></a-plane>
<a-plane position="9.7 2.5 -2" rotation="0 -90 0" width="4" height="3"
  color="#1b1b2b"></a-plane>

10) Test on desktop + mobile (2–3 min)

  • Desktop: Move with WASD/Arrows, look around with mouse, hover images.
  • Mobile: Open the file via a local web server (Live Server). Tap the VR goggles button to enter XR if your device supports it.

11) Troubleshooting (quick fixes)

Nothing shows / black scene

  • Check for duplicated <html> / <head> / <body> tags. You should have one of each.
  • Check image paths: the console (F12) will show 404s if paths are wrong.

Hover doesn't work

  • Ensure images have class="interactable".
  • Cursor must include rayOrigin: mouse.

Too dark

  • Increase ambient or directional light intensity.

Off-by-one Z values

  • Keep frames slightly in front of the wall (e.g., -5.99) so they're visible.

12) Stretch goals (no JS required)

  • Add a sky: <a-sky color="#0a0a14"></a-sky> or a 360° src.
  • Add subtle rotation on hover:
    animation__rot_in="property: rotation; to: 0 0 2; startEvents: 
      mouseenter; dur: 180"
    animation__rot_out="property: rotation; to: 0 0 0; startEvents: 
      mouseleave; dur: 180"
  • Add floor tiles by using a textured image in assets and material="src:#tile; repeat: 10 10".

✅ Final Checklist

  • 9 images inside /images and all src IDs match
  • Frames arranged in 3×3 grid, hover animation works
  • WASD/Arrows movement + cursor hover on desktop
  • Clean single HTML document (no duplicate <html> or <body>)

S.Nocilla 2025 — A-Frame Gallery Tutorial