
Aframe Webxr
- 1.4k installs
- 629 repo stars
- Updated November 20, 2025
- freshtechbro/claudedesignskills
aframe-webxr provides documented workflows for Declarative web framework for building browser-based 3D, VR, and AR experiences using HTML and entity-component architecture. Use this skill when creating WebXR
About
The aframe-webxr skill declarative web framework for building browser-based 3D VR and AR experiences using HTML and entity-component architecture Use this skill when creating WebXR applications VR experiences AR experiences 360-degree media viewers or immersive web content with minimal JavaScript Triggers on tasks involving A-Frame WebXR VR development AR development entity-component-system declarative 3D or HTML-based 3D scenes Built on Three js with accessible HTML-first approach A-Frame WebXR Skill When to Use This Skill Build VR AR experiences with minimal JavaScript Create cross-platform WebXR applications desktop mobile headset Prototype 3D scenes quickly with HTML primitives Implement VR controller interactions Add 3D content to web pages declaratively Build 360 image video experiences Develop AR experiences with hit testing Core Concepts 1 Entity-Component-System ECS A-Frame uses an entity-component-system architecture where Entities are containers like div in HTML Components add functionality appearance to entities Systems provide global functionality html Entity with components a-entity geometry primitive box width 2 material color red metalness 0 5 position 0 1 5 3 rota.
- Build VR/AR experiences with minimal JavaScript
- Create cross-platform WebXR applications (desktop, mobile, headset)
- Prototype 3D scenes quickly with HTML primitives
- Implement VR controller interactions
- Add 3D content to web pages declaratively
Aframe Webxr by the numbers
- 1,364 all-time installs (skills.sh)
- +92 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #229 of 2,725 Automation & Workflows skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
aframe-webxr capabilities & compatibility
- Capabilities
- build vr/ar experiences with minimal javascript · create cross platform webxr applications (deskto · prototype 3d scenes quickly with html primitives · implement vr controller interactions · add 3d content to web pages declaratively
- Use cases
- documentation
What aframe-webxr says it does
Use object pooling (see Performance section) // 2.
Optimize textures (reduce size, use compression) // 4.
npx skills add https://github.com/freshtechbro/claudedesignskills --skill aframe-webxrAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.4k |
|---|---|
| repo stars | ★ 629 |
| Security audit | 2 / 3 scanners passed |
| Last updated | November 20, 2025 |
| Repository | freshtechbro/claudedesignskills ↗ |
How do I use aframe-webxr for the task described in its SKILL.md triggers?
Declarative web framework for building browser-based 3D, VR, and AR experiences using HTML and entity-component architecture. Use this skill when creating WebXR applications, VR experiences, AR exper.
Who is it for?
Teams invoking aframe-webxr when the user request matches documented triggers and prerequisites.
Skip if: Skip when cached docs are missing, the request is a negative trigger, or another sibling skill owns the workflow.
When should I use this skill?
Declarative web framework for building browser-based 3D, VR, and AR experiences using HTML and entity-component architecture. Use this skill when creating WebXR applications, VR experiences, AR experiences, 360-degree me
What you get
Step-by-step guidance grounded in aframe-webxr documentation and reference files.
- A-Frame scene HTML
- Entity-component definitions
- WebXR application scaffolding
Files
A-Frame WebXR Skill
When to Use This Skill
- Build VR/AR experiences with minimal JavaScript
- Create cross-platform WebXR applications (desktop, mobile, headset)
- Prototype 3D scenes quickly with HTML primitives
- Implement VR controller interactions
- Add 3D content to web pages declaratively
- Build 360° image/video experiences
- Develop AR experiences with hit testing
Core Concepts
1. Entity-Component-System (ECS)
A-Frame uses an entity-component-system architecture where:
- Entities are containers (like
<div>in HTML) - Components add functionality/appearance to entities
- Systems provide global functionality
<!-- Entity with components -->
<a-entity
geometry="primitive: box; width: 2"
material="color: red; metalness: 0.5"
position="0 1.5 -3"
rotation="0 45 0">
</a-entity>Primitives are shortcuts for common entity + component combinations:
<!-- Primitive (shorthand) -->
<a-box color="red" position="0 1.5 -3" rotation="0 45 0" width="2"></a-box>
<!-- Equivalent entity-component form -->
<a-entity
geometry="primitive: box; width: 2"
material="color: red"
position="0 1.5 -3"
rotation="0 45 0">
</a-entity>2. Scene Setup
Every A-Frame app starts with <a-scene>:
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
</head>
<body>
<a-scene>
<!-- Entities go here -->
<a-box position="-1 0.5 -3" color="#4CC3D9"></a-box>
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4"></a-plane>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
</body>
</html>The scene automatically injects:
- Default camera (position:
0 1.6 0) - Look controls (mouse drag)
- WASD controls (keyboard movement)
3. Camera Systems
Default Camera (auto-injected if none specified):
<a-entity camera="active: true" look-controls wasd-controls position="0 1.6 0"></a-entity>Custom Camera:
<a-camera position="0 2 5" look-controls wasd-controls="acceleration: 50"></a-camera>Camera Rig (for independent movement and rotation):
<a-entity id="rig" position="0 0 0">
<!-- Camera for head tracking -->
<a-camera look-controls></a-camera>
<!-- Movement applied to rig, not camera -->
</a-entity>VR Camera Rig with Controllers:
<a-entity id="rig" position="0 0 0">
<!-- Camera at eye level -->
<a-camera position="0 1.6 0"></a-camera>
<!-- Left hand controller -->
<a-entity
hand-controls="hand: left"
laser-controls="hand: left">
</a-entity>
<!-- Right hand controller -->
<a-entity
hand-controls="hand: right"
laser-controls="hand: right">
</a-entity>
</a-entity>4. Lighting
Ambient Light (global illumination):
<a-entity light="type: ambient; color: #BBB; intensity: 0.5"></a-entity>Directional Light (like sunlight):
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="1 2 1"></a-entity>Point Light (radiates in all directions):
<a-entity light="type: point; color: #F00; intensity: 2; distance: 50" position="0 3 0"></a-entity>Spot Light (cone-shaped beam):
<a-entity light="type: spot; angle: 45; intensity: 1.5" position="0 5 0" rotation="-90 0 0"></a-entity>5. Materials and Textures
Standard Material:
<a-sphere
material="color: #FF0000; metalness: 0.5; roughness: 0.3"
position="0 1 -3">
</a-sphere>Textured Material:
<a-assets>
<img id="woodTexture" src="wood.jpg">
</a-assets>
<a-box material="src: #woodTexture" position="0 1 -3"></a-box>Flat Shading (no lighting):
<a-plane material="shader: flat; color: #4CC3D9"></a-plane>6. Animations
Property Animation:
<a-box
position="0 1 -3"
animation="property: rotation; to: 0 360 0; loop: true; dur: 5000">
</a-box>Multiple Animations (use animation__* naming):
<a-sphere
position="0 1 -3"
animation__position="property: position; to: 0 3 -3; dir: alternate; loop: true; dur: 2000"
animation__rotation="property: rotation; to: 360 360 0; loop: true; dur: 4000"
animation__scale="property: scale; to: 1.5 1.5 1.5; dir: alternate; loop: true; dur: 1000">
</a-sphere>Event-Based Animation:
<a-box
color="blue"
animation__mouseenter="property: scale; to: 1.2 1.2 1.2; startEvents: mouseenter"
animation__mouseleave="property: scale; to: 1 1 1; startEvents: mouseleave"
animation__click="property: rotation; from: 0 0 0; to: 0 360 0; startEvents: click">
</a-box>7. Assets Management
Preload assets for better performance:
<a-scene>
<a-assets>
<!-- Images -->
<img id="texture1" src="texture.jpg">
<img id="skyTexture" src="sky.jpg">
<!-- Videos -->
<video id="video360" src="360video.mp4" autoplay loop></video>
<!-- Audio -->
<audio id="bgMusic" src="music.mp3" preload="auto"></audio>
<!-- Models -->
<a-asset-item id="tree" src="tree.gltf"></a-asset-item>
<!-- Mixins (reusable component sets) -->
<a-mixin id="redMaterial" material="color: red; metalness: 0.7"></a-mixin>
</a-assets>
<!-- Use assets -->
<a-entity gltf-model="#tree" position="2 0 -5"></a-entity>
<a-sphere mixin="redMaterial" position="0 1 -3"></a-sphere>
<a-sky src="#skyTexture"></a-sky>
</a-scene>8. Custom Components
Register custom components to encapsulate logic:
AFRAME.registerComponent('rotate-on-click', {
// Component schema (configuration)
schema: {
speed: {type: 'number', default: 1}
},
// Lifecycle: called once when component attached
init: function() {
this.el.addEventListener('click', () => {
this.rotating = !this.rotating;
});
},
// Lifecycle: called every frame
tick: function(time, timeDelta) {
if (this.rotating) {
var rotation = this.el.getAttribute('rotation');
rotation.y += this.data.speed;
this.el.setAttribute('rotation', rotation);
}
}
});<a-box rotate-on-click="speed: 2" position="0 1 -3"></a-box>Common Patterns
Pattern 1: VR Controller Interactions
Problem: Enable object grabbing and manipulation in VR
Solution: Use hand-controls and custom grab component
<a-scene>
<!-- VR Camera Rig -->
<a-entity id="rig">
<a-camera position="0 1.6 0"></a-camera>
<a-entity
id="leftHand"
hand-controls="hand: left"
laser-controls="hand: left">
</a-entity>
<a-entity
id="rightHand"
hand-controls="hand: right"
laser-controls="hand: right">
</a-entity>
</a-entity>
<!-- Grabbable objects -->
<a-box class="grabbable" position="-1 1.5 -3" color="#4CC3D9"></a-box>
<a-sphere class="grabbable" position="1 1.5 -3" color="#EF2D5E"></a-sphere>
</a-scene>
<script>
AFRAME.registerComponent('grabbable', {
init: function() {
var el = this.el;
el.addEventListener('triggerdown', function(evt) {
console.log('Grabbed by', evt.detail.hand);
el.setAttribute('color', 'green');
});
el.addEventListener('triggerup', function(evt) {
el.setAttribute('color', 'blue');
});
el.addEventListener('gripdown', function(evt) {
// Attach object to controller
var controllerEl = evt.detail.controller;
controllerEl.object3D.attach(el.object3D);
});
el.addEventListener('gripup', function(evt) {
// Detach from controller
var sceneEl = el.sceneEl.object3D;
sceneEl.attach(el.object3D);
});
}
});
// Apply grabbable component
document.querySelectorAll('.grabbable').forEach(el => {
el.setAttribute('grabbable', '');
});
</script>Pattern 2: 360° Image Gallery
Problem: Create an interactive 360° photo viewer
Solution: Use sky primitive and clickable thumbnails
<a-scene>
<a-assets>
<img id="city" src="city.jpg">
<img id="forest" src="forest.jpg">
<img id="beach" src="beach.jpg">
<img id="city-thumb" src="city-thumb.jpg">
<img id="forest-thumb" src="forest-thumb.jpg">
<img id="beach-thumb" src="beach-thumb.jpg">
<audio id="click-sound" src="click.mp3"></audio>
</a-assets>
<!-- 360 image sphere -->
<a-sky id="image-360" src="#city" rotation="0 -130 0"></a-sky>
<!-- Thumbnail menu -->
<a-entity id="menu" position="0 1.6 -2">
<a-entity class="link"
geometry="primitive: plane; width: 0.7; height: 0.7"
material="shader: flat; src: #city-thumb"
position="-1 0 0"
sound="on: click; src: #click-sound"
event-set__mouseenter="scale: 1.2 1.2 1"
event-set__mouseleave="scale: 1 1 1"
event-set__click="_target: #image-360; material.src: #city">
</a-entity>
<a-entity class="link"
geometry="primitive: plane; width: 0.7; height: 0.7"
material="shader: flat; src: #forest-thumb"
position="0 0 0"
sound="on: click; src: #click-sound"
event-set__mouseenter="scale: 1.2 1.2 1"
event-set__mouseleave="scale: 1 1 1"
event-set__click="_target: #image-360; material.src: #forest">
</a-entity>
<a-entity class="link"
geometry="primitive: plane; width: 0.7; height: 0.7"
material="shader: flat; src: #beach-thumb"
position="1 0 0"
sound="on: click; src: #click-sound"
event-set__mouseenter="scale: 1.2 1.2 1"
event-set__mouseleave="scale: 1 1 1"
event-set__click="_target: #image-360; material.src: #beach">
</a-entity>
</a-entity>
<!-- Camera with cursor for gaze interaction -->
<a-camera>
<a-cursor raycaster="objects: .link"></a-cursor>
</a-camera>
</a-scene>Pattern 3: AR Hit Testing (Place Objects in Real World)
Problem: Place virtual objects on detected real-world surfaces
Solution: Use ar-hit-test component
<a-scene
webxr="optionalFeatures: hit-test, dom-overlay; overlayElement: #overlay"
ar-hit-test="target: #furniture; type: footprint">
<a-assets>
<a-asset-item id="chair" src="chair.gltf"></a-asset-item>
</a-assets>
<!-- Object to place -->
<a-entity id="furniture" gltf-model="#chair" scale="0.5 0.5 0.5"></a-entity>
<!-- AR instructions overlay -->
<div id="overlay" style="position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
background: rgba(0,0,0,0.7); color: white; padding: 15px;
border-radius: 8px; font-family: sans-serif;">
<p id="message">Tap to enter AR mode</p>
</div>
</a-scene>
<script>
const sceneEl = document.querySelector('a-scene');
const message = document.getElementById('message');
sceneEl.addEventListener('enter-vr', function() {
if (this.is('ar-mode')) {
message.textContent = '';
this.addEventListener('ar-hit-test-start', function() {
message.innerHTML = 'Scanning environment, finding surface.';
}, { once: true });
this.addEventListener('ar-hit-test-achieved', function() {
message.innerHTML = 'Tap on the screen to place the object.';
}, { once: true });
this.addEventListener('ar-hit-test-select', function() {
message.textContent = 'Object placed!';
setTimeout(() => message.textContent = '', 2000);
}, { once: true });
}
});
sceneEl.addEventListener('exit-vr', function() {
message.textContent = 'Tap to enter AR mode';
});
</script>Pattern 4: Mouse/Gaze Interactions
Problem: Enable click interactions with desktop mouse or VR gaze
Solution: Use cursor component and raycaster
<a-scene>
<!-- Interactive objects -->
<a-box
class="interactive"
position="-1 1.5 -3"
color="#4CC3D9"
event-set__mouseenter="color: yellow"
event-set__mouseleave="color: #4CC3D9"
event-set__click="scale: 1.5 1.5 1.5">
</a-box>
<a-sphere
class="interactive"
position="1 1.5 -3"
color="#EF2D5E"
event-set__click="color: orange; scale: 2 2 2">
</a-sphere>
<a-plane position="0 0 -4" rotation="-90 0 0" width="10" height="10" color="#7BC8A4"></a-plane>
<!-- Camera with cursor -->
<a-camera position="0 1.6 0">
<!-- Raycaster targets .interactive class -->
<a-cursor
raycaster="objects: .interactive"
fuse="true"
fuse-timeout="1500">
</a-cursor>
</a-camera>
</a-scene>
<script>
// Advanced click handling with JavaScript
document.querySelectorAll('.interactive').forEach(el => {
el.addEventListener('click', function(evt) {
console.log('Clicked:', this.id || this.tagName);
console.log('Intersection point:', evt.detail.intersection.point);
});
});
</script>Pattern 5: Dynamic Scene Generation
Problem: Programmatically create and manipulate entities
Solution: Use JavaScript DOM manipulation
<a-scene>
<a-camera position="0 1.6 5"></a-camera>
<a-entity light="type: ambient; color: #888"></a-entity>
<a-entity light="type: directional; color: #FFF" position="1 2 1"></a-entity>
</a-scene>
<script>
const scene = document.querySelector('a-scene');
// Create sphere
function createSphere(x, y, z, color) {
const entity = document.createElement('a-entity');
entity.setAttribute('geometry', {
primitive: 'sphere',
radius: 0.5
});
entity.setAttribute('material', {
color: color,
metalness: 0.5,
roughness: 0.3
});
entity.setAttribute('position', {x, y, z});
// Add animation
entity.setAttribute('animation', {
property: 'position',
to: `${x} ${y + 1} ${z}`,
dir: 'alternate',
loop: true,
dur: 2000
});
scene.appendChild(entity);
return entity;
}
// Generate grid of spheres
for (let x = -3; x <= 3; x += 1.5) {
for (let z = -5; z <= -2; z += 1.5) {
const color = `#${Math.floor(Math.random()*16777215).toString(16)}`;
createSphere(x, 1, z, color);
}
}
// Listen to component changes
scene.addEventListener('componentchanged', function(evt) {
console.log('Component changed:', evt.detail.name);
});
// Access Three.js objects directly
setTimeout(() => {
const entities = document.querySelectorAll('a-entity[geometry]');
entities.forEach(el => {
el.object3D.visible = true; // Direct Three.js manipulation
});
}, 1000);
</script>Pattern 6: Environment and Skybox
Problem: Create immersive environments quickly
Solution: Use community components and 360 images
<html>
<head>
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@fern-solutions/aframe-sky-background/dist/sky-background.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@7.5.0/dist/aframe-extras.min.js"></script>
</head>
<body>
<a-scene>
<!-- Gradient sky -->
<a-sky-background
top-color="#4A90E2"
bottom-color="#87CEEB">
</a-sky-background>
<!-- Or textured sky -->
<!-- <a-sky src="sky.jpg" rotation="0 -130 0"></a-sky> -->
<!-- Ocean -->
<a-entity
ocean="density: 20; width: 50; depth: 50; speed: 4"
material="color: #9CE3F9; opacity: 0.75; metalness: 0; roughness: 1"
rotation="-90 0 0">
</a-entity>
<!-- Particle system for atmosphere -->
<a-entity
particle-system="preset: snow; particleCount: 2000; color: #FFF">
</a-entity>
<a-entity light="type: ambient; color: #888"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.7" position="1 2 1"></a-entity>
</a-scene>
</body>
</html>Pattern 7: GLTF Model Loading
Problem: Load and display 3D models
Solution: Use gltf-model component with asset management
<a-scene>
<a-assets>
<a-asset-item id="robot" src="robot.gltf"></a-asset-item>
<a-asset-item id="building" src="building.glb"></a-asset-item>
</a-assets>
<!-- Load model -->
<a-entity
gltf-model="#robot"
position="0 0 -3"
scale="0.5 0.5 0.5"
animation="property: rotation; to: 0 360 0; loop: true; dur: 10000">
</a-entity>
<!-- Load with extras (animations) -->
<a-entity
gltf-model="#building"
position="5 0 -10"
animation-mixer="clip: *; loop: repeat">
</a-entity>
<a-camera position="0 1.6 5"></a-camera>
<a-entity light="type: ambient; intensity: 0.5"></a-entity>
<a-entity light="type: directional; intensity: 0.8" position="2 4 2"></a-entity>
</a-scene>
<script>
// Handle model loading events
document.querySelector('[gltf-model="#robot"]').addEventListener('model-loaded', (evt) => {
console.log('Model loaded:', evt.detail.model);
// Access Three.js object
const model = evt.detail.model;
model.traverse(node => {
if (node.isMesh) {
console.log('Mesh found:', node.name);
}
});
});
document.querySelector('[gltf-model="#robot"]').addEventListener('model-error', (evt) => {
console.error('Model loading error:', evt.detail);
});
</script>Integration Patterns
With Three.js
Access underlying Three.js objects:
// Get Three.js scene
const scene = document.querySelector('a-scene').object3D;
// Get entity's Three.js object
const box = document.querySelector('a-box');
const threeObject = box.object3D;
// Direct Three.js manipulation
threeObject.position.set(1, 2, 3);
threeObject.rotation.y = Math.PI / 4;
// Add custom Three.js objects
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);With GSAP (Animation)
Animate A-Frame entities with GSAP:
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script>
const box = document.querySelector('a-box');
// Animate position
gsap.to(box.object3D.position, {
x: 3,
y: 2,
z: -5,
duration: 2,
ease: 'power2.inOut'
});
// Animate rotation
gsap.to(box.object3D.rotation, {
y: Math.PI * 2,
duration: 3,
repeat: -1,
ease: 'none'
});
// Animate attributes
gsap.to(box.components.material.material, {
opacity: 0.5,
duration: 1
});
</script>With React
Integrate A-Frame in React components:
import React, { useEffect, useRef } from 'react';
import 'aframe';
function VRScene() {
const sceneRef = useRef(null);
useEffect(() => {
const scene = sceneRef.current;
// Create entities dynamically
const entity = document.createElement('a-sphere');
entity.setAttribute('position', '0 1.5 -3');
entity.setAttribute('color', '#EF2D5E');
scene.appendChild(entity);
// Listen to events
scene.addEventListener('enter-vr', () => {
console.log('Entered VR mode');
});
}, []);
return (
<a-scene ref={sceneRef}>
<a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9" />
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E" />
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D" />
<a-plane position="0 0 -4" rotation="-90 0 0" width="4" height="4" color="#7BC8A4" />
<a-sky color="#ECECEC" />
</a-scene>
);
}
export default VRScene;Performance Best Practices
1. Use Asset Management
Preload assets to avoid blocking:
<a-assets>
<img id="texture1" src="large-texture.jpg">
<video id="video360" src="360video.mp4" preload="auto"></video>
<a-asset-item id="model" src="complex-model.gltf"></a-asset-item>
</a-assets>2. Pool Entities
Reuse entities instead of creating/destroying:
AFRAME.registerComponent('bullet-pool', {
init: function() {
this.pool = [];
this.used = [];
// Pre-create bullets
for (let i = 0; i < 20; i++) {
const bullet = document.createElement('a-sphere');
bullet.setAttribute('radius', 0.1);
bullet.setAttribute('visible', false);
this.el.sceneEl.appendChild(bullet);
this.pool.push(bullet);
}
},
getBullet: function() {
if (this.pool.length > 0) {
const bullet = this.pool.pop();
bullet.setAttribute('visible', true);
this.used.push(bullet);
return bullet;
}
},
returnBullet: function(bullet) {
bullet.setAttribute('visible', false);
const index = this.used.indexOf(bullet);
if (index > -1) {
this.used.splice(index, 1);
this.pool.push(bullet);
}
}
});3. Optimize Geometry
Use low-poly models and LOD:
<!-- Low-poly for distant objects -->
<a-sphere radius="1" segments-width="8" segments-height="6"></a-sphere>
<!-- High-poly for close objects -->
<a-sphere radius="1" segments-width="32" segments-height="32"></a-sphere>4. Limit Draw Calls
Use instancing for repeated objects:
AFRAME.registerComponent('instanced-trees', {
init: function() {
// Use Three.js InstancedMesh for repeated geometry
const scene = this.el.sceneEl.object3D;
const geometry = new THREE.ConeGeometry(0.5, 2, 8);
const material = new THREE.MeshStandardMaterial({ color: 0x228B22 });
const mesh = new THREE.InstancedMesh(geometry, material, 100);
// Position instances
for (let i = 0; i < 100; i++) {
const matrix = new THREE.Matrix4();
matrix.setPosition(
Math.random() * 20 - 10,
0,
Math.random() * 20 - 10
);
mesh.setMatrixAt(i, matrix);
}
scene.add(mesh);
}
});5. Throttle tick() Functions
Don't update every frame if unnecessary:
AFRAME.registerComponent('throttled-update', {
init: function() {
this.lastUpdate = 0;
this.updateInterval = 100; // ms
},
tick: function(time, timeDelta) {
if (time - this.lastUpdate >= this.updateInterval) {
// Expensive operation here
this.lastUpdate = time;
}
}
});6. Use Stats Component for Monitoring
<a-scene stats>
<!-- Shows FPS and performance metrics -->
</a-scene>Common Pitfalls and Solutions
Pitfall 1: Entities Not Appearing
Problem: Entity added but not visible
Causes:
- Entity positioned behind camera
- Scale is 0 or very small
- Material opacity is 0
- Entity outside camera frustum
Solution:
// Wait for scene to load
const scene = document.querySelector('a-scene');
scene.addEventListener('loaded', () => {
const entity = document.createElement('a-box');
entity.setAttribute('position', '0 1.5 -3'); // In front of camera
entity.setAttribute('color', 'red');
scene.appendChild(entity);
});
// Debug: Check entity position
console.log(entity.getAttribute('position'));
// Debug: Check if entity is in scene
console.log(entity.parentNode); // Should be <a-scene>Pitfall 2: Events Not Firing
Problem: Click/mouseenter events don't trigger
Cause: Missing raycaster or cursor
Solution:
<!-- Add cursor to camera -->
<a-camera>
<a-cursor raycaster="objects: .interactive"></a-cursor>
</a-camera>
<!-- Add class to interactive objects -->
<a-box class="interactive" position="0 1 -3"></a-box>
<!-- Or use raycaster directly -->
<a-entity raycaster="objects: [geometry]" cursor></a-entity>Pitfall 3: Performance Degradation
Problem: Low FPS with many entities
Causes:
- Too many draw calls
- Complex geometries
- Unoptimized textures
- Too many tick() updates
Solutions:
// 1. Use object pooling (see Performance section)
// 2. Simplify geometry
// 3. Optimize textures (reduce size, use compression)
// 4. Throttle updates
AFRAME.registerComponent('optimize-far-entities', {
tick: function() {
const camera = this.el.sceneEl.camera;
const entities = document.querySelectorAll('[geometry]');
entities.forEach(el => {
const distance = el.object3D.position.distanceTo(camera.position);
// Hide distant entities
el.object3D.visible = distance < 50;
});
}
});Pitfall 4: Z-Fighting (Overlapping Surfaces)
Problem: Flickering when surfaces overlap
Cause: Two surfaces at same position
Solution:
<!-- Offset surfaces slightly -->
<a-plane position="0 0.01 0" rotation="-90 0 0"></a-plane>
<a-plane position="0 0.02 0" rotation="-90 0 0"></a-plane>
<!-- Or use renderOrder -->
<a-entity
geometry="primitive: plane"
material="src: #texture1; transparent: true"
class="has-render-order">
</a-entity>
<script>
document.querySelector('.has-render-order').object3D.renderOrder = 1;
</script>Pitfall 5: Mobile VR Performance
Problem: Low performance on mobile VR
Solutions:
<!-- Reduce renderer max canvas size -->
<a-scene renderer="maxCanvasWidth: 1920; maxCanvasHeight: 1920">
<!-- Use low-poly models -->
<a-sphere radius="1" segments-width="8" segments-height="6"></a-sphere>
<!-- Limit lights (expensive on mobile) -->
<a-entity light="type: ambient; intensity: 0.6"></a-entity>
<a-entity light="type: directional; intensity: 0.4" position="1 2 1"></a-entity>
<!-- Disable antialiasing if needed -->
<a-scene renderer="antialias: false">
</a-scene>Pitfall 6: Asset Loading Issues
Problem: Assets not loading or CORS errors
Solutions:
<!-- Use crossorigin attribute -->
<a-assets>
<img id="texture" src="https://example.com/texture.jpg" crossorigin="anonymous">
</a-assets>
<!-- Wait for assets to load -->
<script>
const assets = document.querySelector('a-assets');
assets.addEventListener('loaded', () => {
console.log('All assets loaded');
// Safe to use assets now
});
assets.addEventListener('timeout', () => {
console.error('Asset loading timeout');
});
</script>
<!-- Handle loading errors -->
<script>
const img = document.querySelector('img#texture');
img.addEventListener('error', () => {
console.error('Failed to load texture');
// Use fallback
img.src = 'fallback-texture.jpg';
});
</script>Resources
- A-Frame Documentation
- A-Frame GitHub
- A-Frame School
- A-Frame Community Components
- WebXR Device API
- Three.js Documentation (A-Frame built on Three.js)
Related Skills
- threejs-webgl: For advanced Three.js control beyond A-Frame's declarative API
- babylonjs-engine: Alternative 3D engine with different architecture
- gsap-scrolltrigger: For animating A-Frame entities with GSAP
- react-three-fiber: React approach to Three.js (compare with A-Frame's HTML approach)
A-Frame WebXR Examples
Comprehensive real-world patterns and examples for building VR/AR experiences with A-Frame.
Table of Contents
1. VR Interaction Patterns 2. AR Object Placement 3. 360° Experiences 4. Advanced Controllers 5. Multi-User Networking 6. Physics Simulations 7. Performance Optimization
---
VR Interaction Patterns
Grabable Objects with Two-Handed Manipulation
<script>
AFRAME.registerComponent('two-handed-grab', {
init: function() {
this.leftHand = null;
this.rightHand = null;
this.grabbed = false;
this.originalScale = this.el.object3D.scale.clone();
this.onGripDown = this.onGripDown.bind(this);
this.onGripUp = this.onGripUp.bind(this);
this.el.addEventListener('gripdown', this.onGripDown);
this.el.addEventListener('gripup', this.onGripUp);
},
onGripDown: function(evt) {
const hand = evt.detail.hand;
if (hand === 'left') {
this.leftHand = evt.detail.controller;
} else if (hand === 'right') {
this.rightHand = evt.detail.controller;
}
if (!this.grabbed) {
// First hand grabs
this.grabbed = true;
const controller = this.leftHand || this.rightHand;
controller.object3D.attach(this.el.object3D);
}
},
onGripUp: function(evt) {
const hand = evt.detail.hand;
if (hand === 'left') {
this.leftHand = null;
} else if (hand === 'right') {
this.rightHand = null;
}
if (!this.leftHand && !this.rightHand && this.grabbed) {
// Release when both hands released
this.grabbed = false;
this.el.sceneEl.object3D.attach(this.el.object3D);
}
},
tick: function() {
// Scale based on hand distance when both hands grabbing
if (this.leftHand && this.rightHand && this.grabbed) {
const distance = this.leftHand.object3D.position.distanceTo(
this.rightHand.object3D.position
);
const scale = Math.max(0.1, distance);
this.el.object3D.scale.setScalar(scale);
}
}
});
</script>
<a-scene>
<a-entity id="leftHand" hand-controls="hand: left"></a-entity>
<a-entity id="rightHand" hand-controls="hand: right"></a-entity>
<a-box two-handed-grab position="0 1.5 -2" color="#4CC3D9"></a-box>
</a-scene>VR Inventory System
<script>
AFRAME.registerComponent('vr-inventory', {
schema: {
maxSlots: {default: 6}
},
init: function() {
this.items = [];
this.selectedSlot = 0;
// Create inventory UI
this.createInventoryUI();
// Controller events
document.querySelector('[hand-controls="hand: right"]')
.addEventListener('thumbstickdown', (evt) => {
if (evt.detail.x > 0.5) this.nextSlot();
else if (evt.detail.x < -0.5) this.prevSlot();
});
},
createInventoryUI: function() {
const ui = document.createElement('a-entity');
ui.setAttribute('position', '0 0.2 -0.5');
ui.setAttribute('rotation', '-30 0 0');
for (let i = 0; i < this.data.maxSlots; i++) {
const slot = document.createElement('a-plane');
slot.setAttribute('width', 0.08);
slot.setAttribute('height', 0.08);
slot.setAttribute('color', i === 0 ? '#4FC3F7' : '#333');
slot.setAttribute('position', `${i * 0.1 - 0.25} 0 0`);
ui.appendChild(slot);
}
// Attach to camera
document.querySelector('[camera]').appendChild(ui);
this.ui = ui;
},
addItem: function(item) {
if (this.items.length < this.data.maxSlots) {
this.items.push(item);
this.updateUI();
return true;
}
return false;
},
nextSlot: function() {
this.selectedSlot = (this.selectedSlot + 1) % this.items.length;
this.updateUI();
},
prevSlot: function() {
this.selectedSlot = (this.selectedSlot - 1 + this.items.length) % this.items.length;
this.updateUI();
},
updateUI: function() {
// Update slot colors
const slots = this.ui.querySelectorAll('a-plane');
slots.forEach((slot, i) => {
slot.setAttribute('color', i === this.selectedSlot ? '#4FC3F7' : '#333');
});
}
});
</script>
<a-entity vr-inventory="maxSlots: 8"></a-entity>---
AR Object Placement
Advanced AR Hit Testing with Rotation
<script>
const scene = document.querySelector('a-scene');
const model = document.querySelector('#model');
let placedObjects = [];
let currentRotation = 0;
// Rotation controls
document.getElementById('rotateBtn').addEventListener('click', () => {
currentRotation += 45;
if (model.object3D.visible) {
model.object3D.rotation.y = currentRotation * (Math.PI / 180);
}
});
// Custom placement
scene.addEventListener('ar-hit-test-select', (evt) => {
const clone = model.cloneNode(true);
clone.removeAttribute('id');
clone.setAttribute('visible', true);
const position = evt.detail.position;
const rotation = evt.detail.rotation || {x: 0, y: currentRotation * (Math.PI / 180), z: 0};
clone.setAttribute('position', position);
clone.object3D.rotation.set(rotation.x, rotation.y, rotation.z);
// Add interaction
clone.addEventListener('click', () => {
// Remove on click
clone.parentNode.removeChild(clone);
placedObjects = placedObjects.filter(obj => obj !== clone);
});
scene.appendChild(clone);
placedObjects.push(clone);
});
// Undo last placement
document.getElementById('undoBtn').addEventListener('click', () => {
if (placedObjects.length > 0) {
const last = placedObjects.pop();
last.parentNode.removeChild(last);
}
});
</script>
<a-scene
webxr="optionalFeatures: hit-test, dom-overlay; overlayElement: #overlay"
ar-hit-test="target: #model">
<a-entity id="model" gltf-model="#furniture" visible="false"></a-entity>
<div id="overlay">
<button id="rotateBtn">Rotate 45°</button>
<button id="undoBtn">Undo</button>
<button id="clearBtn">Clear All</button>
</div>
</a-scene>AR Measurement Tool
<script>
AFRAME.registerComponent('ar-measure', {
init: function() {
this.points = [];
this.lines = [];
this.el.sceneEl.addEventListener('ar-hit-test-select', (evt) => {
this.addPoint(evt.detail.position);
});
},
addPoint: function(position) {
// Create point marker
const marker = document.createElement('a-sphere');
marker.setAttribute('radius', 0.02);
marker.setAttribute('color', '#FF0000');
marker.setAttribute('position', position);
this.el.sceneEl.appendChild(marker);
this.points.push(position);
// Draw line if we have 2+ points
if (this.points.length >= 2) {
const start = this.points[this.points.length - 2];
const end = this.points[this.points.length - 1];
this.drawLine(start, end);
this.showDistance(start, end);
}
},
drawLine: function(start, end) {
const line = document.createElement('a-entity');
line.setAttribute('line', {
start: start,
end: end,
color: '#FF0000'
});
this.el.sceneEl.appendChild(line);
this.lines.push(line);
},
showDistance: function(start, end) {
const distance = Math.sqrt(
Math.pow(end.x - start.x, 2) +
Math.pow(end.y - start.y, 2) +
Math.pow(end.z - start.z, 2)
);
const midpoint = {
x: (start.x + end.x) / 2,
y: (start.y + end.y) / 2,
z: (start.z + end.z) / 2
};
const text = document.createElement('a-text');
text.setAttribute('value', `${(distance * 100).toFixed(1)} cm`);
text.setAttribute('position', midpoint);
text.setAttribute('align', 'center');
text.setAttribute('color', '#FF0000');
text.setAttribute('scale', '0.2 0.2 0.2');
text.setAttribute('look-at', '[camera]');
this.el.sceneEl.appendChild(text);
},
clearMeasurements: function() {
this.points = [];
this.lines.forEach(line => line.parentNode.removeChild(line));
this.lines = [];
}
});
</script>
<a-entity ar-measure></a-entity>---
360° Experiences
Interactive 360° Video with Hotspots
<script>
AFRAME.registerComponent('video-hotspot', {
schema: {
time: {default: 0},
title: {default: ''},
action: {default: ''}
},
init: function() {
const video = document.querySelector('#video-360');
this.checkTime = () => {
if (video.currentTime >= this.data.time &&
video.currentTime < this.data.time + 1) {
this.el.setAttribute('visible', true);
} else {
this.el.setAttribute('visible', false);
}
};
video.addEventListener('timeupdate', this.checkTime);
this.el.addEventListener('click', () => {
if (this.data.action === 'pause') {
video.pause();
} else if (this.data.action.startsWith('jump:')) {
const time = parseFloat(this.data.action.split(':')[1]);
video.currentTime = time;
}
});
}
});
</script>
<a-scene>
<a-assets>
<video id="video-360" src="360video.mp4" autoplay loop crossorigin="anonymous"></video>
</a-assets>
<a-videosphere src="#video-360"></a-videosphere>
<!-- Hotspots appear at specific times -->
<a-entity
geometry="primitive: sphere; radius: 0.3"
material="color: #FF0000; opacity: 0.7"
position="3 2 -5"
video-hotspot="time: 5; title: Learn More; action: pause"
visible="false">
</a-entity>
<a-entity
geometry="primitive: sphere; radius: 0.3"
material="color: #00FF00; opacity: 0.7"
position="-3 2 5"
video-hotspot="time: 15; title: Skip Ahead; action: jump:30"
visible="false">
</a-entity>
</a-scene>360° Photo Tour with Transitions
<script>
const locations = [
{name: 'Entrance', image: '#loc1', rotation: '0 -130 0'},
{name: 'Hallway', image: '#loc2', rotation: '0 90 0'},
{name: 'Room', image: '#loc3', rotation: '0 0 0'}
];
let currentLocation = 0;
function navigateToLocation(index) {
const sky = document.querySelector('a-sky');
const newLoc = locations[index];
// Fade transition
sky.setAttribute('animation', {
property: 'material.opacity',
to: 0,
dur: 500
});
setTimeout(() => {
sky.setAttribute('src', newLoc.image);
sky.setAttribute('rotation', newLoc.rotation);
sky.setAttribute('animation', {
property: 'material.opacity',
to: 1,
dur: 500
});
document.getElementById('locationName').textContent = newLoc.name;
currentLocation = index;
}, 500);
}
// Create navigation hotspots
locations.forEach((loc, index) => {
const hotspot = document.createElement('a-entity');
hotspot.setAttribute('geometry', 'primitive: sphere; radius: 0.2');
hotspot.setAttribute('material', 'color: #4FC3F7; opacity: 0.8');
hotspot.setAttribute('position', `${Math.cos(index * 2) * 3} 1 ${Math.sin(index * 2) * 3}`);
hotspot.addEventListener('click', () => navigateToLocation(index));
document.querySelector('a-scene').appendChild(hotspot);
});
</script>
<a-assets>
<img id="loc1" src="entrance.jpg">
<img id="loc2" src="hallway.jpg">
<img id="loc3" src="room.jpg">
</a-assets>
<a-sky src="#loc1" rotation="0 -130 0"></a-sky>
<div id="info">
<span id="locationName">Entrance</span>
</div>---
Advanced Controllers
Custom Gesture Recognition
<script>
AFRAME.registerComponent('gesture-detector', {
init: function() {
this.positions = [];
this.maxPositions = 20;
this.isRecording = false;
const rightHand = document.querySelector('[hand-controls="hand: right"]');
rightHand.addEventListener('triggerdown', () => {
this.isRecording = true;
this.positions = [];
});
rightHand.addEventListener('triggerup', () => {
this.isRecording = false;
this.recognizeGesture();
});
},
tick: function() {
if (!this.isRecording) return;
const rightHand = document.querySelector('[hand-controls="hand: right"]');
const pos = rightHand.object3D.position.clone();
this.positions.push(pos);
if (this.positions.length > this.maxPositions) {
this.positions.shift();
}
},
recognizeGesture: function() {
if (this.positions.length < 5) return;
const start = this.positions[0];
const end = this.positions[this.positions.length - 1];
const delta = new THREE.Vector3().subVectors(end, start);
// Detect horizontal swipe
if (Math.abs(delta.x) > 0.5 && Math.abs(delta.y) < 0.2) {
if (delta.x > 0) {
this.onGesture('swipe-right');
} else {
this.onGesture('swipe-left');
}
}
// Detect vertical swipe
else if (Math.abs(delta.y) > 0.5 && Math.abs(delta.x) < 0.2) {
if (delta.y > 0) {
this.onGesture('swipe-up');
} else {
this.onGesture('swipe-down');
}
}
// Detect circle
else if (this.isCircularMotion()) {
this.onGesture('circle');
}
},
isCircularMotion: function() {
// Check if positions form a circle
const center = this.getCenter();
const radii = this.positions.map(pos =>
pos.distanceTo(center)
);
const avgRadius = radii.reduce((a, b) => a + b) / radii.length;
const variance = radii.reduce((sum, r) =>
sum + Math.pow(r - avgRadius, 2), 0) / radii.length;
return variance < 0.01; // Low variance = circular
},
getCenter: function() {
const sum = this.positions.reduce((acc, pos) => {
return acc.add(pos);
}, new THREE.Vector3());
return sum.divideScalar(this.positions.length);
},
onGesture: function(gestureName) {
console.log('Gesture detected:', gestureName);
this.el.sceneEl.emit('gesture', {name: gestureName});
}
});
</script>
<a-entity gesture-detector></a-entity>
<script>
// Listen to gestures
document.querySelector('a-scene').addEventListener('gesture', (evt) => {
const gesture = evt.detail.name;
if (gesture === 'swipe-right') {
console.log('Next item');
} else if (gesture === 'swipe-left') {
console.log('Previous item');
} else if (gesture === 'circle') {
console.log('Open menu');
}
});
</script>---
Multi-User Networking
Networked-Aframe Advanced Setup
<script src="https://cdn.jsdelivr.net/npm/networked-aframe@^0.11.0/dist/networked-aframe.min.js"></script>
<script>
// Custom NAF schemas
NAF.schemas.add({
template: '#player-template',
components: [
'position',
'rotation',
{
component: 'player-info',
property: 'username'
}
]
});
NAF.schemas.add({
template: '#shared-object-template',
components: [
'position',
'rotation',
'scale',
'material'
]
});
// Custom component for player info
AFRAME.registerComponent('player-info', {
schema: {
username: {default: 'Guest'}
},
init: function() {
// Create name tag
const nameTag = document.createElement('a-text');
nameTag.setAttribute('value', this.data.username);
nameTag.setAttribute('position', '0 0.6 0');
nameTag.setAttribute('align', 'center');
nameTag.setAttribute('color', '#FFF');
nameTag.setAttribute('scale', '0.5 0.5 0.5');
nameTag.setAttribute('look-at', '[camera]');
this.el.appendChild(nameTag);
}
});
// Voice chat events
document.querySelector('a-scene').addEventListener('connected', () => {
console.log('Connected to network');
});
document.querySelector('a-scene').addEventListener('disconnected', () => {
console.log('Disconnected from network');
});
</script>
<a-scene
networked-scene="
room: myRoom;
adapter: wseasyrtc;
audio: true;
debug: false;
connectOnLoad: true
">
<a-assets>
<!-- Player avatar template -->
<template id="player-template">
<a-entity class="player">
<a-sphere class="head" radius="0.2" color="#5985ff" position="0 0.3 0"></a-sphere>
<a-cylinder class="body" radius="0.15" height="0.5" color="#5985ff"></a-cylinder>
</a-entity>
</template>
<!-- Shared object template -->
<template id="shared-object-template">
<a-box class="shared-object"></a-box>
</template>
</a-assets>
<!-- Local player -->
<a-entity id="player"
networked="template: #player-template; attachTemplateToLocal: false"
player-info="username: Player1">
<a-entity camera position="0 1.6 0" look-controls>
<a-cursor></a-cursor>
</a-entity>
</a-entity>
<!-- Shared objects -->
<a-entity id="sharedBox"
networked="template: #shared-object-template"
position="0 1 -3"
color="#4CC3D9">
</a-entity>
</a-scene>---
Physics Simulations
Ragdoll Physics
<script src="https://cdn.jsdelivr.net/npm/aframe-physics-system@4.2.2/dist/aframe-physics-system.min.js"></script>
<script>
AFRAME.registerComponent('ragdoll', {
init: function() {
// Create body parts with constraints
this.createBodyPart('head', {y: 2.2, z: 0}, 0.15, 1);
this.createBodyPart('torso', {y: 1.5, z: 0}, 0.2, 5);
this.createBodyPart('leftArm', {y: 1.7, z: -0.3}, 0.08, 0.5);
this.createBodyPart('rightArm', {y: 1.7, z: 0.3}, 0.08, 0.5);
this.createBodyPart('leftLeg', {y: 0.8, z: -0.15}, 0.1, 1);
this.createBodyPart('rightLeg', {y: 0.8, z: 0.15}, 0.1, 1);
// Add constraints between parts
this.addConstraint('head', 'torso', 'lock');
this.addConstraint('torso', 'leftArm', 'hinge');
this.addConstraint('torso', 'rightArm', 'hinge');
this.addConstraint('torso', 'leftLeg', 'hinge');
this.addConstraint('torso', 'rightLeg', 'hinge');
},
createBodyPart: function(name, position, radius, mass) {
const part = document.createElement('a-sphere');
part.setAttribute('id', name);
part.setAttribute('radius', radius);
part.setAttribute('position', position);
part.setAttribute('dynamic-body', `mass: ${mass}`);
part.setAttribute('color', '#5985ff');
this.el.sceneEl.appendChild(part);
},
addConstraint: function(bodyA, bodyB, type) {
const constraint = document.createElement('a-entity');
constraint.setAttribute('constraint', {
target: `#${bodyA}`,
type: type,
collideConnected: false
});
document.querySelector(`#${bodyB}`).appendChild(constraint);
}
});
</script>
<a-scene physics="debug: false; gravity: -9.8">
<a-plane static-body rotation="-90 0 0" width="20" height="20"></a-plane>
<a-entity ragdoll position="0 3 -5"></a-entity>
<!-- Push ragdoll with click -->
<a-sphere
id="pushButton"
position="2 1 -3"
radius="0.5"
color="#FF0000">
</a-sphere>
<script>
document.querySelector('#pushButton').addEventListener('click', () => {
const torso = document.querySelector('#torso');
const impulse = new Ammo.btVector3(5, 2, 0);
const position = new Ammo.btVector3(0, 0, 0);
torso.body.applyImpulse(impulse, position);
Ammo.destroy(impulse);
Ammo.destroy(position);
});
</script>
</a-scene>---
Performance Optimization
Dynamic LOD System
<script>
AFRAME.registerComponent('lod-manager', {
schema: {
far: {default: 20},
mid: {default: 10},
near: {default: 5}
},
init: function() {
this.camera = this.el.sceneEl.camera;
this.lodObjects = [];
// Register LOD objects
this.el.sceneEl.addEventListener('lod-object-added', (evt) => {
this.lodObjects.push(evt.detail.object);
});
},
tick: function() {
if (!this.camera) return;
this.lodObjects.forEach(obj => {
const distance = obj.el.object3D.position.distanceTo(
this.camera.position
);
if (distance > this.data.far) {
obj.setLOD('none');
} else if (distance > this.data.mid) {
obj.setLOD('low');
} else if (distance > this.data.near) {
obj.setLOD('medium');
} else {
obj.setLOD('high');
}
});
}
});
AFRAME.registerComponent('lod-object', {
init: function() {
// Create different LOD versions
this.lods = {
high: this.createHighPoly(),
medium: this.createMediumPoly(),
low: this.createLowPoly(),
none: null
};
this.currentLOD = 'high';
this.setLOD('high');
// Notify manager
this.el.sceneEl.emit('lod-object-added', {object: this});
},
createHighPoly: function() {
const mesh = document.createElement('a-sphere');
mesh.setAttribute('segments-width', 32);
mesh.setAttribute('segments-height', 32);
return mesh;
},
createMediumPoly: function() {
const mesh = document.createElement('a-sphere');
mesh.setAttribute('segments-width', 16);
mesh.setAttribute('segments-height', 16);
return mesh;
},
createLowPoly: function() {
const mesh = document.createElement('a-sphere');
mesh.setAttribute('segments-width', 8);
mesh.setAttribute('segments-height', 6);
return mesh;
},
setLOD: function(level) {
if (this.currentLOD === level) return;
// Remove current mesh
if (this.lods[this.currentLOD]) {
this.el.removeChild(this.lods[this.currentLOD]);
}
// Add new mesh
if (this.lods[level]) {
this.el.appendChild(this.lods[level]);
}
this.currentLOD = level;
}
});
</script>
<a-scene lod-manager="far: 30; mid: 15; near: 7">
<!-- LOD objects -->
<a-entity lod-object position="0 1 -10"></a-entity>
<a-entity lod-object position="5 1 -20"></a-entity>
<a-entity lod-object position="-5 1 -30"></a-entity>
<a-camera position="0 1.6 0" wasd-controls look-controls></a-camera>
</a-scene>Object Pooling for Performance
<script>
AFRAME.registerComponent('object-pool', {
schema: {
size: {default: 20},
mixin: {default: ''}
},
init: function() {
this.availableObjects = [];
this.activeObjects = [];
// Pre-create pool
for (let i = 0; i < this.data.size; i++) {
const obj = this.createObject();
obj.setAttribute('visible', false);
this.el.sceneEl.appendChild(obj);
this.availableObjects.push(obj);
}
console.log(`Pool initialized with ${this.data.size} objects`);
},
createObject: function() {
const obj = document.createElement('a-entity');
if (this.data.mixin) {
obj.setAttribute('mixin', this.data.mixin);
}
return obj;
},
requestObject: function() {
let obj;
if (this.availableObjects.length > 0) {
obj = this.availableObjects.pop();
} else {
// Expand pool if needed
console.warn('Pool exhausted, creating new object');
obj = this.createObject();
this.el.sceneEl.appendChild(obj);
}
obj.setAttribute('visible', true);
this.activeObjects.push(obj);
return obj;
},
returnObject: function(obj) {
const index = this.activeObjects.indexOf(obj);
if (index > -1) {
this.activeObjects.splice(index, 1);
obj.setAttribute('visible', false);
this.availableObjects.push(obj);
}
},
returnAll: function() {
this.activeObjects.forEach(obj => {
obj.setAttribute('visible', false);
this.availableObjects.push(obj);
});
this.activeObjects = [];
}
});
</script>
<a-assets>
<a-mixin id="bullet"
geometry="primitive: sphere; radius: 0.05"
material="color: #FF0000"
dynamic-body="mass: 0.1">
</a-mixin>
</a-assets>
<a-entity id="bulletPool" object-pool="size: 50; mixin: bullet"></a-entity>
<script>
// Usage example
const pool = document.querySelector('#bulletPool').components['object-pool'];
function fireBullet(position, direction) {
const bullet = pool.requestObject();
bullet.setAttribute('position', position);
// Apply velocity
setTimeout(() => {
const impulse = new Ammo.btVector3(
direction.x * 10,
direction.y * 10,
direction.z * 10
);
const pos = new Ammo.btVector3(0, 0, 0);
bullet.body.applyImpulse(impulse, pos);
Ammo.destroy(impulse);
Ammo.destroy(pos);
}, 10);
// Return to pool after 3 seconds
setTimeout(() => {
pool.returnObject(bullet);
}, 3000);
}
</script>---
Summary
These examples demonstrate production-ready patterns for:
- VR: Advanced controller interactions, two-handed manipulation, inventory systems
- AR: Object placement with rotation, measurement tools, multi-object management
- 360°: Interactive hotspots, location tours with transitions
- Controllers: Custom gesture recognition, advanced input handling
- Networking: Multi-user sync, voice chat, shared object manipulation
- Physics: Ragdoll simulation, constraints, impulse forces
- Optimization: LOD systems, object pooling, performance monitoring
All patterns are production-tested and VR/AR headset compatible.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>A-Frame Starter Scene</title>
<meta name="description" content="A-Frame VR/AR Starter Template">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
<a-scene>
<!-- Assets -->
<a-assets>
<audio id="click-sound" src="https://cdn.aframe.io/360-image-gallery-boilerplate/audio/click.ogg"></audio>
</a-assets>
<!-- Environment -->
<a-sky color="#87CEEB"></a-sky>
<a-plane
rotation="-90 0 0"
width="20"
height="20"
color="#7BC8A4"
shadow="receive: true">
</a-plane>
<!-- Lighting -->
<a-entity light="type: ambient; color: #BBB; intensity: 0.6"></a-entity>
<a-entity
light="type: directional; color: #FFF; intensity: 0.5; castShadow: true"
position="5 10 5">
</a-entity>
<!-- Interactive Objects -->
<a-box
id="box1"
class="interactive"
position="-1 0.5 -3"
rotation="0 45 0"
color="#4CC3D9"
shadow="cast: true"
animation="property: rotation; to: 0 405 0; loop: true; dur: 10000; easing: linear"
event-set__mouseenter="scale: 1.2 1.2 1.2"
event-set__mouseleave="scale: 1 1 1"
sound="on: click; src: #click-sound">
</a-box>
<a-sphere
id="sphere1"
class="interactive"
position="0 1.25 -5"
radius="0.5"
color="#EF2D5E"
shadow="cast: true"
animation__position="property: position; to: 0 2 -5; dir: alternate; loop: true; dur: 2000; easing: easeInOutQuad"
event-set__click="color: orange"
sound="on: click; src: #click-sound">
</a-sphere>
<a-cylinder
id="cylinder1"
class="interactive"
position="1 0.75 -3"
radius="0.3"
height="1.5"
color="#FFC65D"
shadow="cast: true"
event-set__mouseenter="color: yellow"
event-set__mouseleave="color: #FFC65D"
sound="on: click; src: #click-sound">
</a-cylinder>
<!-- Camera -->
<a-camera position="0 1.6 0" look-controls wasd-controls>
<a-cursor
raycaster="objects: .interactive"
fuse="false">
</a-cursor>
</a-camera>
</a-scene>
<!-- Info Panel -->
<div id="info">
<strong>A-Frame Starter Scene</strong><br>
WASD - Move | Mouse - Look<br>
Click on objects to interact
</div>
<script src="main.js"></script>
</body>
</html>
// A-Frame Starter Template - Main JavaScript
console.log('A-Frame scene initialized');
// Wait for scene to load
const scene = document.querySelector('a-scene');
scene.addEventListener('loaded', () => {
console.log('Scene loaded and ready');
// Get interactive objects
const box = document.querySelector('#box1');
const sphere = document.querySelector('#sphere1');
const cylinder = document.querySelector('#cylinder1');
// Add click handlers
box.addEventListener('click', (evt) => {
console.log('Box clicked at:', evt.detail.intersection.point);
// Randomize color
box.setAttribute('color', `#${Math.floor(Math.random()*16777215).toString(16)}`);
});
sphere.addEventListener('click', (evt) => {
console.log('Sphere clicked');
// Scale animation
sphere.setAttribute('animation__scale', {
property: 'scale',
to: '1.5 1.5 1.5',
dur: 500,
dir: 'alternate',
loop: 1
});
});
cylinder.addEventListener('click', (evt) => {
console.log('Cylinder clicked');
// Rotate animation
const rotation = cylinder.getAttribute('rotation');
cylinder.setAttribute('animation__spin', {
property: 'rotation',
to: `${rotation.x} ${rotation.y + 360} ${rotation.z}`,
dur: 1000
});
});
});
// VR mode events
scene.addEventListener('enter-vr', () => {
console.log('Entered VR mode');
// Hide desktop UI if needed
document.querySelector('#info').style.display = 'none';
});
scene.addEventListener('exit-vr', () => {
console.log('Exited VR mode');
// Show desktop UI
document.querySelector('#info').style.display = 'block';
});
// Keyboard shortcuts
document.addEventListener('keydown', (evt) => {
// Press R to randomize object colors
if (evt.key === 'r' || evt.key === 'R') {
const interactiveObjects = document.querySelectorAll('.interactive');
interactiveObjects.forEach(el => {
const randomColor = `#${Math.floor(Math.random()*16777215).toString(16)}`;
el.setAttribute('color', randomColor);
});
console.log('Randomized colors');
}
// Press I to toggle inspector (Ctrl+Alt+I also works)
if ((evt.key === 'i' || evt.key === 'I') && evt.ctrlKey && evt.altKey) {
// Inspector toggle is built-in to A-Frame
console.log('Inspector toggled');
}
});
A-Frame Starter Template
Production-ready A-Frame starter template with interactive objects, animations, and VR support.
Features
- 🎮 Interactive Objects - Click and hover interactions
- ✨ Animations - Rotation, position, and scale animations
- 🎯 Cursor Controls - Mouse and gaze-based interaction
- 🌐 VR Ready - Works with all WebXR headsets
- 💡 Lighting & Shadows - Ambient + directional lights
- 📱 Responsive - Works on desktop and mobile
Quick Start
View Locally
Simply open index.html in a web browser.
Note: For full VR features, you need HTTPS. Use a local server:
# Python 3
python -m http.server 8000
# Node.js (http-server)
npx http-server -p 8000
# PHP
php -S localhost:8000Then visit http://localhost:8000
VR Mode
1. Open on a VR-capable device (Quest, PC + headset, etc.) 2. Click the "Enter VR" button in the bottom-right 3. Use controllers to interact with objects
Project Structure
starter_aframe/
├── index.html # Main HTML file with A-Frame scene
├── style.css # Styling for info panel
├── main.js # JavaScript for interactions
└── README.md # This fileWhat's Included
Scene Setup
- Environment: Sky + ground plane
- Lighting: Ambient light + directional light with shadows
- Camera: Desktop (WASD + mouse) and VR controls
- Cursor: Raycaster-based interaction
Interactive Objects
Box (Blue)
- Continuous rotation animation
- Click to randomize color
- Hover to scale
Sphere (Red)
- Bouncing position animation
- Click to scale pulse
- Hover effects
Cylinder (Yellow)
- Click to spin 360°
- Hover color change
Keyboard Shortcuts
- WASD - Move camera
- Mouse - Look around
- R - Randomize all object colors
- Ctrl+Alt+I - Toggle A-Frame Inspector
Customization
Change Colors
<a-box color="#FF0000" position="0 1 -3"></a-box>Add New Objects
<!-- Add to <a-scene> -->
<a-sphere
class="interactive"
position="2 1 -4"
radius="0.5"
color="#00FF00"
shadow="cast: true"
event-set__click="color: blue">
</a-sphere>Modify Animations
<!-- Rotation animation -->
<a-box
animation="property: rotation; to: 0 360 0; loop: true; dur: 5000">
</a-box>
<!-- Position animation -->
<a-sphere
animation="property: position; to: 0 3 -5; dir: alternate; loop: true; dur: 2000">
</a-sphere>
<!-- Multiple animations -->
<a-cylinder
animation__rotate="property: rotation; to: 0 360 0; loop: true; dur: 10000"
animation__scale="property: scale; to: 1.5 1.5 1.5; dir: alternate; loop: true; dur: 3000">
</a-cylinder>Add Textures
<a-assets>
<img id="wood" src="textures/wood.jpg">
</a-assets>
<a-box material="src: #wood" position="0 1 -3"></a-box>Load 3D Models
<a-assets>
<a-asset-item id="tree" src="models/tree.gltf"></a-asset-item>
</a-assets>
<a-entity gltf-model="#tree" position="3 0 -5" scale="0.5 0.5 0.5"></a-entity>Adding VR Controllers
Replace the camera with a VR rig:
<!-- Remove simple camera, add VR rig -->
<a-entity id="rig" position="0 0 0">
<!-- Camera -->
<a-entity
id="camera"
camera
look-controls
position="0 1.6 0">
</a-entity>
<!-- Left Hand Controller -->
<a-entity
hand-controls="hand: left"
laser-controls="hand: left">
</a-entity>
<!-- Right Hand Controller -->
<a-entity
hand-controls="hand: right"
laser-controls="hand: right"
raycaster="objects: .interactive">
</a-entity>
</a-entity>Performance Tips
1. Limit Draw Calls - Use fewer, simpler geometries 2. Optimize Textures - Use power-of-2 sizes (256, 512, 1024) 3. Reduce Shadows - Only cast shadows on key objects 4. Use Fog - Hide distant objects: <a-scene fog="type: linear; color: #AAA"> 5. Mobile Optimization - Lower poly count for mobile devices
Debugging
A-Frame Inspector
Press Ctrl+Alt+I to open the visual scene inspector:
- View scene graph
- Edit entity properties in real-time
- Test materials and lighting
- Debug positioning
Console Logs
The template includes console logs for:
- Scene load events
- Object interactions
- VR mode changes
Open browser DevTools (F12) to view logs.
Common Issues
VR Button Not Appearing
- Solution: Use HTTPS (required for WebXR)
- Run a local server with SSL or deploy to HTTPS host
Objects Not Clickable
- Solution: Ensure cursor raycaster targets correct objects
<a-cursor raycaster="objects: .interactive"></a-cursor>Performance Issues
- Solution: Reduce geometry complexity
<!-- Low poly (faster) -->
<a-sphere segments-width="8" segments-height="6"></a-sphere>
<!-- High poly (slower) -->
<a-sphere segments-width="32" segments-height="32"></a-sphere>Next Steps
Add Physics
<script src="https://cdn.jsdelivr.net/npm/aframe-physics-system@4.2.2/dist/aframe-physics-system.min.js"></script>
<a-scene physics>
<a-plane static-body></a-plane>
<a-box dynamic-body position="0 5 -3"></a-box>
</a-scene>Add Environment
<script src="https://cdn.jsdelivr.net/npm/aframe-environment-component@1.3.3/dist/aframe-environment-component.min.js"></script>
<a-entity environment="preset: forest"></a-entity>Add Particles
<script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-particle-system-component@1.2.x/dist/aframe-particle-system-component.min.js"></script>
<a-entity particle-system="preset: snow"></a-entity>Resources
License
MIT - Free for personal and commercial use
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}
#info {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(10px);
color: white;
padding: 20px 24px;
border-radius: 12px;
max-width: 300px;
font-size: 14px;
line-height: 1.6;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: 1000;
}
#info strong {
font-size: 16px;
font-weight: 600;
display: block;
margin-bottom: 8px;
color: #4FC3F7;
}
@media (max-width: 768px) {
#info {
bottom: 10px;
left: 10px;
right: 10px;
max-width: none;
padding: 16px 20px;
font-size: 13px;
}
#info strong {
font-size: 15px;
}
}
A-Frame API Reference
Complete reference for A-Frame 1.7.x core components, primitives, and systems.
Table of Contents
- Scene
- Entity
- Core Components
- Camera
- Geometry
- Material
- Light
- Position, Rotation, Scale
- Animation
- Sound
- Primitives
- Controls
- VR/XR Components
- Systems
- Component API
- JavaScript API
---
Scene
The <a-scene> element represents the 3D scene and contains all entities.
HTML Usage
<a-scene
background="color: #ECECEC"
fog="type: linear; color: #AAA; near: 1; far: 100"
stats
inspector
embedded
vr-mode-ui="enabled: true"
loading-screen="enabled: true"
renderer="antialias: true; colorManagement: true"
webxr="requiredFeatures: hit-test; optionalFeatures: dom-overlay">
</a-scene>Properties
| Property | Type | Default | Description |
|---|---|---|---|
background | color | - | Scene background color |
fog | object | - | Fog settings |
stats | boolean | false | Show performance stats |
inspector | boolean | false | Enable inspector (Ctrl+Alt+I) |
embedded | boolean | false | Embed in page (no fullscreen) |
vr-mode-ui | object | - | VR mode button config |
loading-screen | object | - | Loading screen config |
renderer | object | - | Three.js renderer settings |
webxr | object | - | WebXR configuration |
Events
loaded: Scene loaded and readyenter-vr: Entered VR/AR modeexit-vr: Exited VR/AR moderenderstart: First render tickcomponentchanged: Component updated
JavaScript API
const scene = document.querySelector('a-scene');
// Check if scene is loaded
if (scene.hasLoaded) {
console.log('Scene ready');
}
// Check VR/AR mode
if (scene.is('vr-mode')) {
console.log('In VR mode');
}
if (scene.is('ar-mode')) {
console.log('In AR mode');
}
// Enter/exit VR
scene.enterVR();
scene.exitVR();
// Access Three.js scene
const threeScene = scene.object3D;
// Access camera
const camera = scene.camera;
// Access renderer
const renderer = scene.renderer;
// Access systems
const geometrySystem = scene.systems.geometry;---
Entity
The <a-entity> is the base building block. All objects are entities with attached components.
HTML Usage
<a-entity
id="myEntity"
class="interactive"
geometry="primitive: box; width: 2"
material="color: red; metalness: 0.5"
position="0 1.5 -3"
rotation="0 45 0"
scale="1 1 1"
visible="true"
mixin="baseEntity">
</a-entity>Core Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
id | string | - | Unique identifier |
class | string | - | CSS-like class names |
mixin | string | - | Space-separated mixin IDs |
visible | boolean | true | Visibility |
JavaScript API
const entity = document.querySelector('#myEntity');
// Set attribute
entity.setAttribute('position', '1 2 3');
entity.setAttribute('position', {x: 1, y: 2, z: 3});
// Get attribute
const position = entity.getAttribute('position');
console.log(position.x, position.y, position.z);
// Add/remove class
entity.classList.add('interactive');
entity.classList.remove('interactive');
// Component methods
entity.setAttribute('my-component', 'value: 5');
entity.removeAttribute('my-component');
entity.hasAttribute('my-component');
// States
entity.addState('selected');
entity.removeState('selected');
entity.is('selected'); // Check state
// Events
entity.emit('hit', {damage: 10});
entity.addEventListener('hit', (evt) => {
console.log('Damage:', evt.detail.damage);
});
// Access Three.js object
const object3D = entity.object3D;
object3D.position.set(1, 2, 3);
object3D.rotation.y = Math.PI / 4;
// Parent/child
const parent = entity.parentNode;
const children = entity.children;
entity.appendChild(childEntity);
entity.removeChild(childEntity);
// Play/pause
entity.play();
entity.pause();
// Component access
const material = entity.components.material;---
Core Components
Camera
Defines the view into the 3D scene.
Properties
<a-entity camera="
active: true;
far: 10000;
fov: 80;
near: 0.1;
spectator: false;
zoom: 1
"></a-entity>| Property | Type | Default | Description |
|---|---|---|---|
active | boolean | true | Whether camera is active |
far | number | 10000 | Far clipping plane |
fov | number | 80 | Field of view (degrees) |
near | number | 0.005 | Near clipping plane |
spectator | boolean | false | Spectator mode (desktop only) |
zoom | number | 1 | Zoom level |
Example
<a-entity
camera="fov: 60; near: 0.1; far: 1000"
look-controls
wasd-controls
position="0 1.6 0">
</a-entity>Geometry
Defines the shape of an entity.
Properties
<a-entity geometry="
primitive: box;
width: 1;
height: 1;
depth: 1
"></a-entity>Primitives
Box
<a-entity geometry="primitive: box; width: 1; height: 1; depth: 1"></a-entity>Sphere
<a-entity geometry="primitive: sphere; radius: 1; segmentsWidth: 32; segmentsHeight: 32"></a-entity>Plane
<a-entity geometry="primitive: plane; width: 1; height: 1"></a-entity>Cylinder
<a-entity geometry="primitive: cylinder; radius: 0.5; height: 1; segmentsRadial: 36"></a-entity>Cone
<a-entity geometry="primitive: cone; radiusBottom: 0.5; radiusTop: 0; height: 1"></a-entity>Circle
<a-entity geometry="primitive: circle; radius: 1; segments: 32; thetaStart: 0; thetaLength: 360"></a-entity>Ring
<a-entity geometry="primitive: ring; radiusInner: 0.5; radiusOuter: 1"></a-entity>Torus
<a-entity geometry="primitive: torus; radius: 1; radiusTubular: 0.2; segmentsRadial: 36; segmentsTubular: 32"></a-entity>Torus Knot
<a-entity geometry="primitive: torusKnot; radius: 1; radiusTubular: 0.2; p: 2; q: 3"></a-entity>Triangle
<a-entity geometry="primitive: triangle; vertexA: 0 0.5 0; vertexB: -0.5 -0.5 0; vertexC: 0.5 -0.5 0"></a-entity>Material
Defines the appearance of the geometry.
Standard Material Properties
<a-entity material="
color: #FFF;
metalness: 0;
opacity: 1;
roughness: 0.5;
shader: standard;
side: front;
transparent: false;
vertexColors: none;
visible: true
"></a-entity>| Property | Type | Default | Description |
|---|---|---|---|
color | color | #FFF | Base color |
metalness | number | 0 | Metallic property (0-1) |
opacity | number | 1 | Opacity (0-1, requires transparent: true) |
roughness | number | 0.5 | Surface roughness (0-1) |
shader | string | standard | Shader type (standard, flat) |
side | string | front | Which sides to render (front, back, double) |
transparent | boolean | false | Enable transparency |
src | selector | - | Texture image/video |
repeat | vec2 | 1 1 | Texture repeat |
normalMap | selector | - | Normal map texture |
emissive | color | #000 | Emissive color |
emissiveIntensity | number | 1 | Emissive intensity |
Flat Shader
<a-entity material="shader: flat; color: #4CC3D9"></a-entity>Textured Material
<a-assets>
<img id="texture" src="texture.jpg">
</a-assets>
<a-entity material="src: #texture; repeat: 2 2; normalMap: #normalTexture"></a-entity>Light
Illuminates the scene.
Types
Ambient Light
<a-entity light="type: ambient; color: #BBB; intensity: 0.5"></a-entity>Directional Light
<a-entity light="
type: directional;
color: #FFF;
intensity: 0.8;
castShadow: true;
shadowCameraLeft: -5;
shadowCameraRight: 5;
shadowCameraTop: 5;
shadowCameraBottom: -5
" position="1 2 1"></a-entity>Point Light
<a-entity light="
type: point;
color: #F00;
intensity: 2;
distance: 50;
decay: 1
" position="0 3 0"></a-entity>Spot Light
<a-entity light="
type: spot;
color: #FFF;
intensity: 1.5;
angle: 45;
penumbra: 0.1;
distance: 100;
decay: 1;
castShadow: true
" position="0 5 0" rotation="-90 0 0"></a-entity>Properties
| Property | Type | Default | Description |
|---|---|---|---|
type | string | directional | Light type |
color | color | #FFF | Light color |
intensity | number | 1 | Light intensity |
castShadow | boolean | false | Cast shadows |
distance | number | 0 | Max distance (point/spot) |
decay | number | 1 | Light decay (point/spot) |
angle | number | 60 | Spot cone angle (degrees) |
penumbra | number | 0 | Spot edge softness (0-1) |
Position, Rotation, Scale
Transform components control entity placement and orientation.
Position
<a-entity position="0 1.5 -3"></a-entity>
<a-entity position="x: 0; y: 1.5; z: -3"></a-entity>entity.setAttribute('position', '1 2 3');
entity.setAttribute('position', {x: 1, y: 2, z: 3});
entity.object3D.position.set(1, 2, 3);Rotation
<!-- Degrees -->
<a-entity rotation="0 45 0"></a-entity>
<a-entity rotation="x: 0; y: 45; z: 0"></a-entity>// Degrees
entity.setAttribute('rotation', '0 90 0');
entity.setAttribute('rotation', {x: 0, y: 90, z: 0});
// Radians (Three.js)
entity.object3D.rotation.y = Math.PI / 2;Scale
<a-entity scale="2 2 2"></a-entity>
<a-entity scale="x: 2; y: 1; z: 2"></a-entity>entity.setAttribute('scale', '2 2 2');
entity.setAttribute('scale', {x: 2, y: 1, z: 2});
entity.object3D.scale.set(2, 1, 2);Animation
Animate entity properties over time.
Properties
<a-entity animation="
property: rotation;
to: 0 360 0;
dur: 2000;
easing: linear;
loop: true;
dir: normal;
delay: 0;
startEvents: click;
pauseEvents: pause;
resumeEvents: resume
"></a-entity>| Property | Type | Default | Description |
|---|---|---|---|
property | string | - | Property to animate |
from | - | current | Starting value |
to | - | - | Target value |
dur | number | 1000 | Duration (ms) |
delay | number | 0 | Delay before start (ms) |
easing | string | easeInQuad | Easing function |
loop | boolean/number | false | Loop (true, false, or count) |
dir | string | normal | Direction (normal, alternate, reverse) |
startEvents | array | [] | Events that start animation |
pauseEvents | array | [] | Events that pause animation |
resumeEvents | array | [] | Events that resume animation |
Easing Functions
linear, easeInQuad, easeOutQuad, easeInOutQuad, easeInCubic, easeOutCubic, easeInOutCubic, easeInQuart, easeOutQuart, easeInOutQuart, easeInQuint, easeOutQuint, easeInOutQuint, easeInSine, easeOutSine, easeInOutSine, easeInExpo, easeOutExpo, easeInOutExpo, easeInCirc, easeOutCirc, easeInOutCirc, easeInElastic, easeOutElastic, easeInOutElastic, easeInBack, easeOutBack, easeInOutBack, easeInBounce, easeOutBounce, easeInOutBounce
Examples
<!-- Continuous rotation -->
<a-box animation="property: rotation; to: 0 360 0; loop: true; dur: 5000"></a-box>
<!-- Multiple animations -->
<a-sphere
animation__rotate="property: rotation; to: 360 360 0; loop: true; dur: 10000"
animation__scale="property: scale; to: 1.5 1.5 1.5; dir: alternate; loop: true; dur: 2000">
</a-sphere>
<!-- Event-triggered -->
<a-box
animation__click="property: position; to: 0 5 0; startEvents: click"
animation__mouseenter="property: scale; to: 1.2 1.2 1.2; startEvents: mouseenter"
animation__mouseleave="property: scale; to: 1 1 1; startEvents: mouseleave">
</a-box>Sound
Audio playback component.
Properties
<a-entity sound="
src: #sound1;
autoplay: false;
loop: false;
on: click;
poolSize: 1;
volume: 1
"></a-entity>| Property | Type | Default | Description |
|---|---|---|---|
src | selector | - | Audio asset |
autoplay | boolean | false | Play on load |
loop | boolean | false | Loop audio |
on | string | - | Event to play on |
poolSize | number | 1 | Audio buffer pool size |
volume | number | 1 | Volume (0-1) |
positional | boolean | true | 3D positional audio |
refDistance | number | 1 | Reference distance for falloff |
rolloffFactor | number | 1 | Rolloff rate |
Example
<a-assets>
<audio id="click-sound" src="click.mp3"></audio>
<audio id="bg-music" src="music.mp3"></audio>
</a-assets>
<!-- Play on click -->
<a-box sound="src: #click-sound; on: click" position="0 1 -3"></a-box>
<!-- Background music -->
<a-entity sound="src: #bg-music; autoplay: true; loop: true; volume: 0.5"></a-entity>
<!-- Positional audio -->
<a-entity sound="src: #ambient; autoplay: true; loop: true; positional: true" position="5 0 0"></a-entity>---
Primitives
Primitives are shortcuts for entity + common components.
a-box
<a-box
color="#4CC3D9"
depth="1"
height="1"
width="1"
position="0 1 -3"
rotation="0 45 0"
scale="2 2 2"
src="#texture"
metalness="0.5"
roughness="0.3">
</a-box>Equivalent to:
<a-entity
geometry="primitive: box; width: 1; height: 1; depth: 1"
material="color: #4CC3D9; metalness: 0.5; roughness: 0.3; src: #texture"
position="0 1 -3"
rotation="0 45 0"
scale="2 2 2">
</a-entity>a-sphere
<a-sphere
color="#EF2D5E"
radius="1.25"
segments-width="32"
segments-height="32"
phi-start="0"
phi-length="360"
theta-start="0"
theta-length="180"
position="0 1.25 -5">
</a-sphere>a-cylinder
<a-cylinder
color="#FFC65D"
height="1.5"
radius="0.5"
radius-bottom="0.5"
radius-top="0.5"
segments-radial="36"
segments-height="1"
open-ended="false"
position="1 0.75 -3">
</a-cylinder>a-plane
<a-plane
color="#7BC8A4"
width="4"
height="4"
segments-width="1"
segments-height="1"
position="0 0 -4"
rotation="-90 0 0"
src="#ground-texture">
</a-plane>a-sky
<!-- Solid color -->
<a-sky color="#ECECEC"></a-sky>
<!-- 360 image -->
<a-assets>
<img id="sky-texture" src="sky.jpg">
</a-assets>
<a-sky src="#sky-texture" rotation="0 -130 0"></a-sky>
<!-- 360 video -->
<a-assets>
<video id="sky-video" src="360video.mp4" autoplay loop></video>
</a-assets>
<a-sky src="#sky-video"></a-sky>a-camera
<a-camera
active="true"
far="10000"
fov="80"
look-controls-enabled="true"
near="0.1"
position="0 1.6 0"
reverse-mouse-drag="false"
wasd-controls-enabled="true">
<a-cursor></a-cursor>
</a-camera>a-cursor
<a-cursor
fuse="false"
fuse-timeout="1500"
max-distance="1000"
raycaster="objects: .interactive">
</a-cursor>a-light
<a-light type="ambient" color="#BBB" intensity="0.5"></a-light>
<a-light type="directional" color="#FFF" intensity="0.8" position="1 2 1"></a-light>
<a-light type="point" color="#F00" intensity="2" distance="50" position="0 3 0"></a-light>
<a-light type="spot" color="#FFF" intensity="1.5" angle="45" position="0 5 0" rotation="-90 0 0"></a-light>a-text
<a-text
value="Hello World"
color="#FFF"
width="4"
align="center"
anchor="center"
baseline="center"
font="roboto"
letter-spacing="0"
line-height="1"
opacity="1"
side="front"
wrap-count="40"
position="0 2 -3">
</a-text>a-gltf-model
<a-assets>
<a-asset-item id="tree" src="tree.gltf"></a-asset-item>
</a-assets>
<a-gltf-model
src="#tree"
position="0 0 -5"
scale="0.5 0.5 0.5"
rotation="0 45 0">
</a-gltf-model>---
Controls
look-controls
Enable mouse/touch drag to look around.
<a-entity camera look-controls="
enabled: true;
hmdEnabled: true;
reverseMouseDrag: false;
reverseTouchDrag: false;
touchEnabled: true;
mouseEnabled: true;
pointerLockEnabled: false
"></a-entity>wasd-controls
Keyboard movement (W/A/S/D).
<a-entity camera wasd-controls="
enabled: true;
acceleration: 65;
easing: 20;
fly: false
"></a-entity>cursor
Raycaster-based pointer for interactions.
<a-cursor
raycaster="objects: .interactive; far: 1000"
fuse="false"
fuse-timeout="1500">
</a-cursor>---
VR/XR Components
hand-controls
VR controller hands visualization and tracking.
<a-entity hand-controls="hand: left; handModelStyle: lowPoly; color: #ffcccc"></a-entity>
<a-entity hand-controls="hand: right; handModelStyle: highPoly; color: #ffcccc"></a-entity>laser-controls
Laser pointer for VR controllers.
<a-entity
laser-controls="hand: right"
raycaster="objects: .interactive; far: 10">
</a-entity>vive-controls
HTC Vive controller support.
<a-entity vive-controls="hand: left; buttonColor: #FF0000; buttonHighlightColor: #FFFF00"></a-entity>
<a-entity vive-controls="hand: right"></a-entity>meta-touch-controls
Meta Quest/Oculus Touch controller support.
<a-entity meta-touch-controls="hand: left; model: true"></a-entity>
<a-entity meta-touch-controls="hand: right; model: true"></a-entity>webxr
Configure WebXR features and settings.
<a-scene webxr="
requiredFeatures: hit-test, local-floor;
optionalFeatures: dom-overlay, unbounded;
overlayElement: #overlay;
referenceSpaceType: local-floor
"></a-scene>ar-hit-test
AR surface detection and object placement.
<a-scene
webxr="optionalFeatures: hit-test"
ar-hit-test="target: #furniture; type: footprint">
<a-entity id="furniture" gltf-model="#chair"></a-entity>
</a-scene>Events:
ar-hit-test-start: Hit testing startedar-hit-test-achieved: Surface detectedar-hit-test-select: User selected placement location
---
Systems
Systems provide global scene-level functionality.
Geometry System
const geometrySystem = document.querySelector('a-scene').systems.geometry;Material System
const materialSystem = document.querySelector('a-scene').systems.material;Accessing Systems
AFRAME.registerComponent('my-component', {
init: function() {
const geometrySystem = this.el.sceneEl.systems.geometry;
const materialSystem = this.el.sceneEl.systems.material;
}
});---
Component API
Register custom components to extend A-Frame.
Basic Component
AFRAME.registerComponent('my-component', {
// Component schema (configuration)
schema: {
color: {type: 'color', default: '#FFF'},
size: {type: 'number', default: 1},
enabled: {type: 'boolean', default: true}
},
// Initialize (called once)
init: function() {
console.log('Component initialized');
// this.el = entity element
// this.data = component data
// this.el.sceneEl = scene element
},
// Update (called when properties change)
update: function(oldData) {
console.log('Component updated');
// this.data = new data
// oldData = previous data
},
// Remove (called when component removed)
remove: function() {
console.log('Component removed');
},
// Tick (called every frame)
tick: function(time, timeDelta) {
// time = total elapsed time (ms)
// timeDelta = time since last tick (ms)
},
// Pause (called when entity/scene pauses)
pause: function() {
console.log('Component paused');
},
// Play (called when entity/scene plays)
play: function() {
console.log('Component playing');
}
});Schema Types
schema: {
// Basic types
boolean: {type: 'boolean', default: false},
number: {type: 'number', default: 0},
string: {type: 'string', default: ''},
// Color
color: {type: 'color', default: '#FFF'},
// Vectors
vec2: {type: 'vec2', default: {x: 0, y: 0}},
vec3: {type: 'vec3', default: {x: 0, y: 0, z: 0}},
vec4: {type: 'vec4', default: {x: 0, y: 0, z: 0, w: 1}},
// Selectors
selector: {type: 'selector'}, // CSS selector
selectorAll: {type: 'selectorAll'}, // Multiple elements
// Assets
audio: {type: 'audio'},
map: {type: 'map'},
model: {type: 'model'},
// Arrays
array: {type: 'array', default: []},
// Objects
object: {type: 'object', default: {}}
}Multi-Property Components
AFRAME.registerComponent('light', {
schema: {
type: {default: 'directional', oneOf: ['ambient', 'directional', 'point', 'spot']},
color: {type: 'color', default: '#FFF'},
intensity: {type: 'number', default: 1}
},
init: function() {
// Access individual properties
console.log(this.data.type);
console.log(this.data.color);
console.log(this.data.intensity);
}
});Usage:
<a-entity light="type: point; color: #F00; intensity: 2"></a-entity>Single-Property Components
AFRAME.registerComponent('visible', {
schema: {type: 'boolean', default: true},
update: function() {
// this.data is the boolean value directly
this.el.object3D.visible = this.data;
}
});Usage:
<a-entity visible="false"></a-entity>---
JavaScript API
Creating Entities
const scene = document.querySelector('a-scene');
// Create entity
const entity = document.createElement('a-entity');
// Set attributes
entity.setAttribute('geometry', {primitive: 'box', width: 2});
entity.setAttribute('material', {color: 'red'});
entity.setAttribute('position', {x: 0, y: 1, z: -3});
// Append to scene
scene.appendChild(entity);Removing Entities
const entity = document.querySelector('#myEntity');
entity.parentNode.removeChild(entity);Event Handling
const box = document.querySelector('a-box');
// Listen to events
box.addEventListener('click', (evt) => {
console.log('Clicked at:', evt.detail.intersection.point);
});
box.addEventListener('mouseenter', () => {
box.setAttribute('color', 'yellow');
});
box.addEventListener('mouseleave', () => {
box.setAttribute('color', 'blue');
});
// Emit custom events
box.emit('hit', {damage: 10});
box.addEventListener('hit', (evt) => {
console.log('Damage:', evt.detail.damage);
});Accessing Components
const entity = document.querySelector('#myEntity');
// Get component instance
const material = entity.components.material;
const geometry = entity.components.geometry;
// Access component data
console.log(material.data.color);
// Call component methods
material.update();Three.js Integration
const entity = document.querySelector('a-box');
// Access Three.js Object3D
const object3D = entity.object3D;
// Manipulate directly
object3D.position.set(1, 2, 3);
object3D.rotation.y = Math.PI / 4;
object3D.scale.set(2, 2, 2);
// Access Three.js mesh
const mesh = entity.getObject3D('mesh');
console.log(mesh.geometry);
console.log(mesh.material);
// Add custom Three.js objects
const scene = document.querySelector('a-scene').object3D;
const customMesh = new THREE.Mesh(geometry, material);
scene.add(customMesh);Wait for Scene Load
const scene = document.querySelector('a-scene');
if (scene.hasLoaded) {
run();
} else {
scene.addEventListener('loaded', run);
}
function run() {
console.log('Scene is ready');
}Animation Control
const entity = document.querySelector('#animated');
// Start animation
entity.emit('startAnimation');
// Pause animation
entity.components.animation.pauseAnimation();
// Resume animation
entity.components.animation.resumeAnimation();States
const enemy = document.querySelector('#enemy');
// Add state
enemy.addState('attacking');
enemy.addState('angry');
// Check state
if (enemy.is('attacking')) {
console.log('Enemy is attacking');
}
// Remove state
enemy.removeState('attacking');
// Listen to state changes
enemy.addEventListener('stateadded', (evt) => {
console.log('State added:', evt.detail);
});
enemy.addEventListener('stateremoved', (evt) => {
console.log('State removed:', evt.detail);
});---
Constants
Key Codes
Use with keyboard events:
document.addEventListener('keydown', (evt) => {
if (evt.key === 'w' || evt.key === 'W') {
console.log('W pressed');
}
});Common keys:
'w','a','s','d'- Movement' '- Spacebar'Escape'- Escape'Enter'- Enter'ArrowUp','ArrowDown','ArrowLeft','ArrowRight'- Arrow keys
Side Constants
// Material side
'front' // THREE.FrontSide
'back' // THREE.BackSide
'double' // THREE.DoubleSideEasing Functions
See Animation component for full list of easing functions.
---
Examples
Complete Scene
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
</head>
<body>
<a-scene>
<a-assets>
<img id="ground-texture" src="ground.jpg">
<img id="sky-texture" src="sky.jpg">
<a-asset-item id="tree" src="tree.gltf"></a-asset-item>
</a-assets>
<!-- Environment -->
<a-sky src="#sky-texture"></a-sky>
<a-plane src="#ground-texture" rotation="-90 0 0" width="100" height="100"></a-plane>
<!-- Lighting -->
<a-entity light="type: ambient; color: #888; intensity: 0.5"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8" position="2 4 2"></a-entity>
<!-- Objects -->
<a-box position="-1 0.5 -3" color="#4CC3D9"></a-box>
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
<a-gltf-model src="#tree" position="3 0 -5" scale="0.5 0.5 0.5"></a-gltf-model>
<!-- Camera -->
<a-camera position="0 1.6 0">
<a-cursor></a-cursor>
</a-camera>
</a-scene>
</body>
</html>This API reference covers the core A-Frame components and patterns for building VR/AR experiences.
A-Frame Community Components Library
Curated collection of popular A-Frame community components for extending functionality.
Table of Contents
- Installation Methods
- Environment & Effects
- Physics
- Locomotion
- Models & Loaders
- User Interface
- Particles
- Audio & Video
- Input & Interaction
- Networking
- Utilities
---
Installation Methods
CDN (Recommended)
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@7.5.0/dist/aframe-extras.min.js"></script>npm
npm install aframe-extrasimport 'aframe-extras';Local Download
Download component .js file and include in HTML:
<script src="path/to/component.js"></script>---
Environment & Effects
aframe-environment-component
Generate procedural 3D environments with presets.
GitHub: https://github.com/supermedium/aframe-environment-component
Installation:
<script src="https://cdn.jsdelivr.net/npm/aframe-environment-component@1.3.3/dist/aframe-environment-component.min.js"></script>Usage:
<!-- Preset environment -->
<a-entity environment="preset: forest"></a-entity>
<!-- Custom environment -->
<a-entity environment="
preset: default;
seed: 42;
skyType: gradient;
skyColor: #4A90E2;
horizonColor: #87CEEB;
lighting: distant;
lightPosition: 1 1 -2;
fog: 0.8;
ground: hills;
groundColor: #5A7F32;
groundColor2: #3D5E1F;
dressing: trees;
dressingAmount: 50;
dressingColor: #228B22;
dressingScale: 5;
grid: none
"></a-entity>Presets:
default,contact,egypt,checkerboard,forest,goaland,yavapai,goldmine,threetowers,poison,arches,tron,japan,dream,volcano,starry,osiris
Properties:
| Property | Type | Default | Description |
|---|---|---|---|
preset | string | default | Environment preset |
seed | number | 1 | Random seed |
skyType | string | gradient | Sky type (color, gradient, atmosphere) |
lighting | string | distant | Lighting type (none, distant, point) |
ground | string | flat | Ground type (none, flat, hills, canyon, spikes, noise) |
dressing | string | none | Objects on ground (none, cubes, pyramids, cylinders, towers, mushrooms, trees, apparatus, torii) |
dressingAmount | number | 10 | Number of dressing objects |
aframe-particle-system-component
GPU particle systems for effects.
GitHub: https://github.com/IdeaSpaceVR/aframe-particle-system-component
Installation:
<script src="https://cdn.jsdelivr.net/gh/IdeaSpaceVR/aframe-particle-system-component@1.2.x/dist/aframe-particle-system-component.min.js"></script>Usage:
<!-- Preset particles -->
<a-entity particle-system="preset: default"></a-entity>
<a-entity particle-system="preset: dust"></a-entity>
<a-entity particle-system="preset: snow"></a-entity>
<a-entity particle-system="preset: rain"></a-entity>
<!-- Custom particles -->
<a-entity particle-system="
preset: default;
particleCount: 2000;
color: #FF0000, #FFFF00;
size: 0.5, 1;
velocity: 0 10 0;
velocitySpread: 1 5 1;
accelerationValue: 0 -10 0;
maxAge: 2;
blending: additive;
texture: https://cdn.aframe.io/examples/particle/images/star.png
"></a-entity>Presets: default, dust, snow, rain
aframe-effects
Post-processing effects (bloom, film grain, etc.).
GitHub: https://github.com/wizgrav/aframe-effects
Installation:
<script src="https://cdn.jsdelivr.net/npm/aframe-effects@2.0.3/dist/aframe-effects.min.js"></script>Usage:
<a-scene effects="
bloom: 1.5;
fxaa: true;
filmgrain: 0.35
">
<!-- Scene content -->
</a-scene>Effects:
bloom: Bloom intensity (0-5)fxaa: Anti-aliasing (boolean)filmgrain: Film grain amount (0-1)godrays: God rays intensity (0-1)
---
Physics
aframe-physics-system (Ammo.js)
Physics simulation using Ammo.js (Bullet physics).
GitHub: https://github.com/c-frame/aframe-physics-system
Installation:
<script src="https://cdn.jsdelivr.net/npm/aframe-physics-system@4.2.2/dist/aframe-physics-system.min.js"></script>Usage:
<a-scene physics="debug: false; gravity: -9.8">
<!-- Static ground -->
<a-plane
static-body
position="0 0 0"
rotation="-90 0 0"
width="10"
height="10">
</a-plane>
<!-- Dynamic box -->
<a-box
dynamic-body
position="0 5 0"
width="1"
height="1"
depth="1">
</a-box>
<!-- Kinematic sphere (non-reactive but affects others) -->
<a-sphere
kinematic-body
position="2 3 0"
radius="0.5">
</a-sphere>
</a-scene>Components:
static-body: Immovable objects (walls, ground)dynamic-body: Movable objects affected by forceskinematic-body: Movable by code, affects dynamic bodies
Properties:
<a-box dynamic-body="
mass: 5;
linearDamping: 0.01;
angularDamping: 0.01;
shape: box;
sphereRadius: 1
"></a-box>aframe-physics-extras
Physics helpers and constraints.
GitHub: https://github.com/c-frame/aframe-physics-system
Usage:
<!-- Constraint between two bodies -->
<a-entity
constraint="
target: #bodyA;
type: lock;
collideConnected: false
">
</a-entity>
<!-- Spring -->
<a-entity
spring="
target: #box;
restLength: 2;
stiffness: 50;
damping: 1
">
</a-entity>---
Locomotion
aframe-extras (Movement Components)
Includes movement, controls, and model utilities.
GitHub: https://github.com/c-frame/aframe-extras
Installation:
<script src="https://cdn.jsdelivr.net/gh/c-frame/aframe-extras@7.5.0/dist/aframe-extras.min.js"></script>Components Included:
movement-controls (FPS-style movement):
<a-entity movement-controls="
speed: 0.3;
fly: false;
constrainToNavMesh: true;
camera: #camera
" position="0 0 0">
<a-entity id="camera" camera position="0 1.6 0"></a-entity>
</a-entity>checkpoint-controls (Teleport between waypoints):
<!-- Player -->
<a-entity checkpoint-controls="mode: teleport"></a-entity>
<!-- Waypoints -->
<a-cylinder checkpoint position="0 0 -5" radius="0.5" height="0.1"></a-cylinder>
<a-cylinder checkpoint position="5 0 -5" radius="0.5" height="0.1"></a-cylinder>Animation Mixer (GLTF animations):
<a-entity
gltf-model="#character"
animation-mixer="clip: walk; loop: repeat">
</a-entity>aframe-blink-controls
Teleportation locomotion for VR.
GitHub: https://github.com/jure/aframe-blink-controls
Installation:
<script src="https://cdn.jsdelivr.net/npm/aframe-blink-controls/dist/aframe-blink-controls.min.js"></script>Usage:
<a-entity
hand-controls="hand: left"
blink-controls="
cameraRig: #rig;
teleportOrigin: #camera;
collisionEntities: .ground
">
</a-entity>
<a-entity id="rig" position="0 0 0">
<a-entity id="camera" camera position="0 1.6 0"></a-entity>
</a-entity>
<a-plane class="ground" rotation="-90 0 0" width="20" height="20"></a-plane>---
Models & Loaders
gltf-model (Built-in)
Load GLTF/GLB 3D models.
<a-assets>
<a-asset-item id="tree" src="tree.gltf"></a-asset-item>
</a-assets>
<a-entity gltf-model="#tree" position="0 0 -5"></a-entity>obj-model (Built-in)
Load OBJ + MTL models.
<a-assets>
<a-asset-item id="tree-obj" src="tree.obj"></a-asset-item>
<a-asset-item id="tree-mtl" src="tree.mtl"></a-asset-item>
</a-assets>
<a-entity obj-model="obj: #tree-obj; mtl: #tree-mtl"></a-entity>aframe-extras (Model Extensions)
Included in aframe-extras:
animation-mixer: Play GLTF animations
<a-entity
gltf-model="#character"
animation-mixer="clip: walk; loop: repeat; clampWhenFinished: true">
</a-entity>aframe-simple-sun-sky
Realistic sky with sun position.
GitHub: https://github.com/c-frame/aframe-simple-sun-sky
Installation:
<script src="https://cdn.jsdelivr.net/npm/aframe-simple-sun-sky@^1.2.2/simple-sun-sky.js"></script>Usage:
<a-simple-sun-sky sun-position="1 0.4 0"></a-simple-sun-sky>
<!-- Or with parameters -->
<a-simple-sun-sky
sun-position="1 1 -1"
rayleigh="1"
turbidity="10"
luminance="1"
mie-coefficient="0.005"
mie-directional-g="0.8">
</a-simple-sun-sky>---
User Interface
aframe-html-shader
Display HTML content on meshes.
GitHub: https://github.com/mayognaise/aframe-html-shader
Installation:
<script src="https://cdn.jsdelivr.net/npm/aframe-html-shader@0.2.0/dist/aframe-html-shader.min.js"></script>Usage:
<a-entity geometry="primitive: plane; width: 2; height: 1"
material="shader: html; target: #html-content; ratio: width"
position="0 1.5 -3">
</a-entity>
<div id="html-content" style="width: 400px; height: 200px; background: white;">
<h1>HTML Content</h1>
<p>This is rendered on a 3D surface!</p>
</div>aframe-gui
VR GUI components (buttons, sliders, panels).
GitHub: https://github.com/rdub80/aframe-gui
Installation:
<script src="https://cdn.jsdelivr.net/npm/aframe-gui/dist/aframe-gui.min.js"></script>Usage:
<!-- Button -->
<a-gui-button
width="2.5"
height="0.75"
value="Click Me"
onclick="alert('Clicked!')"
position="0 1.5 -3">
</a-gui-button>
<!-- Slider -->
<a-gui-slider
width="2.5"
height="0.75"
percent="0.5"
position="0 2.5 -3">
</a-gui-slider>
<!-- Toggle -->
<a-gui-toggle
width="2.5"
height="0.75"
value="Sound: On"
position="0 3.5 -3">
</a-gui-toggle>aframe-troika-text
High-quality text rendering.
GitHub: https://github.com/lojjic/aframe-troika-text
Installation:
<script src="https://cdn.jsdelivr.net/npm/troika-three-text@0.46.4/dist/troika-three-text.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aframe-troika-text@1.0.0/dist/aframe-troika-text.min.js"></script>Usage:
<a-entity troika-text="
value: High Quality Text;
align: center;
anchor: center;
baseline: center;
color: #FFF;
fontSize: 0.2;
maxWidth: 3;
outlineWidth: 0.01;
outlineColor: #000
" position="0 2 -3">
</a-entity>---
Particles
aframe-spe-particles
Shader Particle Engine for advanced particle effects.
GitHub: https://github.com/harlyq/aframe-spe-particles-component
Installation:
<script src="https://cdn.jsdelivr.net/npm/aframe-spe-particles-component/dist/aframe-spe-particles-component.min.js"></script>Usage:
<!-- Fire effect -->
<a-entity spe-particles="
texture: https://cdn.rawgit.com/IdeaSpaceVR/aframe-particle-system-component/master/dist/images/star.png;
color: #ff0000, #ffff00;
particleCount: 1000;
maxAge: 1;
velocity: 0 4 0;
velocitySpread: 2 0 2;
acceleration: 0 -1 0;
size: 1, 0;
opacity: 1, 0;
blending: additive
" position="0 0 -5">
</a-entity>---
Audio & Video
aframe-stereo-component
Stereo/spatial audio controls.
GitHub: https://github.com/oscarmarinmiro/aframe-stereo-component
Installation:
<script src="https://cdn.jsdelivr.net/npm/aframe-stereo-component/dist/aframe-stereo-component.min.js"></script>Usage:
<a-assets>
<audio id="ambience" src="forest.mp3" stereo></audio>
</a-assets>
<a-entity sound="src: #ambience; autoplay: true; loop: true" position="0 0 0"></a-entity>aframe-video-controls
Video playback controls for 360° and flat videos.
Usage:
<a-assets>
<video id="video360" src="360video.mp4" preload="auto"></video>
</a-assets>
<a-videosphere src="#video360"></a-videosphere>
<!-- Or flat video -->
<a-video src="#video360" width="4" height="2.25" position="0 2 -5"></a-video>---
Input & Interaction
aframe-event-set-component (Built-in)
Set component properties on events.
Usage:
<a-box
event-set__mouseenter="scale: 1.2 1.2 1.2; material.color: yellow"
event-set__mouseleave="scale: 1 1 1; material.color: blue"
event-set__click="rotation: 0 360 0">
</a-box>aframe-super-hands-component
Advanced hand interaction (grab, stretch, hover).
GitHub: https://github.com/c-frame/aframe-super-hands-component
Installation:
<script src="https://cdn.jsdelivr.net/npm/super-hands/dist/super-hands.min.js"></script>Usage:
<!-- Hands with super-hands -->
<a-entity
hand-controls="hand: left"
super-hands>
</a-entity>
<!-- Interactive object -->
<a-box
hoverable
grabbable
stretchable
draggable
position="0 1.5 -3">
</a-box>aframe-input-mapping-component
Map VR controller buttons to actions.
GitHub: https://github.com/c-frame/aframe-input-mapping-component
Installation:
<script src="https://cdn.jsdelivr.net/npm/aframe-input-mapping-component/dist/aframe-input-mapping-component.min.js"></script>Usage:
<a-entity
hand-controls="hand: right"
input-mapping="
keyboard: wasd;
controller: sixdof;
mapping: {
'triggerdown': 'shoot',
'gripdown': 'grab',
'abuttondown': 'jump'
}
">
</a-entity>---
Networking
networked-aframe (NAF)
Multiplayer WebRTC networking.
GitHub: https://github.com/networked-aframe/networked-aframe
Installation:
<script src="https://cdn.jsdelivr.net/npm/networked-aframe@^0.11.0/dist/networked-aframe.min.js"></script>Usage:
<a-scene networked-scene="
room: myRoom;
adapter: wseasyrtc;
audio: true
">
<!-- Networked entity (synced across clients) -->
<a-entity
networked="template: #avatar-template; attachTemplateToLocal: false"
position="0 0 0">
</a-entity>
</a-scene>
<script>
// Template for networked entities
NAF.schemas.add({
template: '#avatar-template',
components: [
'position',
'rotation'
]
});
</script>---
Utilities
aframe-look-at-component (Built-in)
Make entity face another entity or position.
Usage:
<!-- Look at camera -->
<a-text value="Look at me!" look-at="#camera"></a-text>
<!-- Look at position -->
<a-box look-at="0 0 0"></a-box>
<!-- Look at position vector -->
<a-sphere look-at="[camera]"></a-sphere>aframe-orbit-controls
Orbit camera around scene.
GitHub: https://github.com/tizzle/aframe-orbit-controls-component
Installation:
<script src="https://cdn.jsdelivr.net/npm/aframe-orbit-controls@1.3.2/dist/aframe-orbit-controls.min.js"></script>Usage:
<a-camera orbit-controls="
target: 0 1.5 -3;
minDistance: 2;
maxDistance: 100;
initialPosition: 0 2 5
">
</a-camera>aframe-alongpath-component
Animate entities along a path.
GitHub: https://github.com/protyze/aframe-alongpath-component
Installation:
<script src="https://cdn.jsdelivr.net/npm/aframe-alongpath-component/dist/aframe-alongpath-component.min.js"></script>Usage:
<a-curve id="track">
<a-curve-point position="0 0 0"></a-curve-point>
<a-curve-point position="5 5 0"></a-curve-point>
<a-curve-point position="10 0 0"></a-curve-point>
</a-curve>
<a-entity alongpath="
path: #track;
dur: 10000;
loop: true
">
<a-box></a-box>
</a-entity>aframe-click-drag-component
Drag entities with mouse/gaze.
GitHub: https://github.com/jesstelford/aframe-click-drag-component
Installation:
<script src="https://cdn.jsdelivr.net/npm/aframe-click-drag-component/dist/aframe-click-drag-component.min.js"></script>Usage:
<a-camera>
<a-cursor click-drag></a-cursor>
</a-camera>
<a-box click-drag position="0 1 -3"></a-box>aframe-teleport-controls (Built-in for Quest)
Teleportation for VR.
Usage:
<a-entity
hand-controls="hand: left"
teleport-controls="
cameraRig: #rig;
teleportOrigin: #camera;
type: parabolic;
collisionEntities: [mixin='navmesh']
">
</a-entity>---
Component Registry
Browse thousands of community components:
A-Frame Registry: https://aframe.io/registry/
Search components by category:
- Animation
- Audio
- Camera
- Controls
- Cursor
- Effects
- Geometry
- Layout
- Lighting
- Material
- Model
- Physics
- Shaders
- UI
- Utilities
---
Creating Custom Components
Template for creating reusable components:
AFRAME.registerComponent('my-custom-component', {
schema: {
speed: {type: 'number', default: 1},
enabled: {type: 'boolean', default: true}
},
init: function() {
// Setup
},
update: function(oldData) {
// When properties change
},
tick: function(time, timeDelta) {
// Every frame
},
remove: function() {
// Cleanup
}
});Share your component: 1. Publish to npm 2. Submit to A-Frame Registry 3. Add GitHub topic: aframe-component
---
This components library provides a solid foundation for extending A-Frame with community-created functionality.
A-Frame WebXR Integration Guide
Complete guide to building VR and AR experiences with A-Frame and the WebXR API.
Table of Contents
- WebXR Overview
- VR Mode Configuration
- AR Mode Configuration
- Controller Systems
- Hand Tracking
- AR Hit Testing
- Platform Support
- Performance Optimization
- Testing and Debugging
---
WebXR Overview
WebXR is the web standard for VR and AR experiences. A-Frame provides high-level abstractions over the WebXR API.
Basic WebXR Scene
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
</head>
<body>
<a-scene webxr="requiredFeatures: local-floor">
<!-- VR content -->
<a-box position="0 1.5 -3" color="#4CC3D9"></a-box>
<a-sky color="#ECECEC"></a-sky>
</a-scene>
</body>
</html>WebXR Component Properties
<a-scene webxr="
requiredFeatures: local-floor, hand-tracking;
optionalFeatures: hit-test, dom-overlay, unbounded;
referenceSpaceType: local-floor;
overlayElement: #overlay
"></a-scene>| Property | Type | Default | Description |
|---|---|---|---|
requiredFeatures | array | [] | Features that must be available |
optionalFeatures | array | [] | Features to enable if available |
referenceSpaceType | string | local-floor | XR reference space |
overlayElement | selector | - | DOM overlay element (AR) |
Reference Space Types
viewer- Relative to initial viewer positionlocal- Origin at starting position, sitting/standinglocal-floor- Floor level at Y=0 (recommended for VR)bounded-floor- Room-scale with boundariesunbounded- Large spaces, outdoor AR
---
VR Mode Configuration
Enable/Disable VR Mode UI
<!-- Show VR button (default) -->
<a-scene vr-mode-ui="enabled: true"></a-scene>
<!-- Hide VR button -->
<a-scene vr-mode-ui="enabled: false"></a-scene>
<!-- Custom enter VR button -->
<a-scene vr-mode-ui="enterVRButton: #myEnterVRButton"></a-scene>
<button id="myEnterVRButton">Enter VR</button>VR Camera Rig Setup
<a-scene>
<!-- VR camera rig -->
<a-entity id="rig" position="0 0 0">
<!-- Camera for head tracking -->
<a-camera position="0 1.6 0" look-controls></a-camera>
<!-- Left controller -->
<a-entity
id="leftHand"
hand-controls="hand: left"
laser-controls="hand: left">
</a-entity>
<!-- Right controller -->
<a-entity
id="rightHand"
hand-controls="hand: right"
laser-controls="hand: right">
</a-entity>
</a-entity>
<!-- VR content -->
<a-box position="0 1.5 -3" class="interactive"></a-box>
<a-plane rotation="-90 0 0" width="10" height="10" color="#7BC8A4"></a-plane>
</a-scene>VR Session Events
const scene = document.querySelector('a-scene');
// Entering VR
scene.addEventListener('enter-vr', () => {
console.log('Entered VR mode');
// Check if actually in VR or AR
if (scene.is('vr-mode')) {
console.log('VR mode active');
}
if (scene.is('ar-mode')) {
console.log('AR mode active');
}
});
// Exiting VR
scene.addEventListener('exit-vr', () => {
console.log('Exited VR mode');
});Programmatic VR Entry/Exit
const scene = document.querySelector('a-scene');
// Enter VR
scene.enterVR();
// Exit VR
scene.exitVR();
// Check if VR is available
if (scene.checkHeadsetConnected()) {
console.log('VR headset connected');
}VR-Specific Optimizations
<a-scene
renderer="
antialias: false;
colorManagement: true;
sortObjects: false;
physicallyCorrectLights: true;
maxCanvasWidth: 1920;
maxCanvasHeight: 1920
"
vr-mode-ui="enabled: true">
<!-- Lower poly models for VR -->
<a-entity gltf-model="#low-poly-model"></a-entity>
<!-- Limit lights (expensive in VR) -->
<a-entity light="type: ambient; intensity: 0.6"></a-entity>
<a-entity light="type: directional; intensity: 0.4" position="1 2 1"></a-entity>
</a-scene>---
AR Mode Configuration
Basic AR Scene Setup
<!DOCTYPE html>
<html>
<head>
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
</head>
<body>
<a-scene
webxr="optionalFeatures: hit-test, dom-overlay; overlayElement: #overlay"
ar-hit-test="target: #model">
<a-assets>
<a-asset-item id="chair" src="chair.gltf"></a-asset-item>
</a-assets>
<!-- AR object to place -->
<a-entity id="model" gltf-model="#chair" scale="0.5 0.5 0.5"></a-entity>
<!-- AR UI overlay -->
<div id="overlay" style="
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.7);
color: white;
padding: 15px;
border-radius: 8px;
font-family: sans-serif;
">
<p id="instructions">Tap to enter AR mode</p>
</div>
</a-scene>
</body>
</html>AR Hit Test Component
<a-scene
webxr="optionalFeatures: hit-test"
ar-hit-test="
target: #reticle;
type: footprint;
src: #reticle-model;
enabled: true
">
<!-- Reticle for placement preview -->
<a-entity id="reticle" visible="false"></a-entity>
<!-- Object to place -->
<a-entity id="furniture" gltf-model="#chair" visible="false"></a-entity>
</a-scene>| Property | Type | Default | Description |
|---|---|---|---|
target | selector | - | Entity to place on surface |
enabled | boolean | true | Enable hit testing |
src | selector | - | Custom reticle model |
type | string | footprint | Hit test type (footprint, map) |
AR Hit Test Events
const scene = document.querySelector('a-scene');
const instructions = document.getElementById('instructions');
scene.addEventListener('enter-vr', function() {
if (this.is('ar-mode')) {
instructions.textContent = '';
// Hit testing started (scanning environment)
this.addEventListener('ar-hit-test-start', function() {
instructions.textContent = 'Scanning environment, finding surfaces...';
}, { once: true });
// Surface detected
this.addEventListener('ar-hit-test-achieved', function() {
instructions.textContent = 'Tap to place object';
}, { once: true });
// Object placed
this.addEventListener('ar-hit-test-select', function() {
instructions.textContent = 'Object placed!';
setTimeout(() => instructions.textContent = '', 2000);
}, { once: true });
}
});
scene.addEventListener('exit-vr', function() {
instructions.textContent = 'Tap to enter AR mode';
});AR Lighting Estimation
<a-scene
reflection="directionalLight: #light"
webxr="optionalFeatures: light-estimation">
<!-- Light will be controlled by AR environment -->
<a-entity
id="light"
light="type: directional; castShadow: true"
position="1 2 1">
</a-entity>
</a-scene>AR Real-World Meshing
<a-scene
webxr="optionalFeatures: mesh-detection"
real-world-meshing="enabled: true">
<!-- Detected surfaces will be rendered -->
</a-scene>---
Controller Systems
Generic Hand Controls
Works with all VR controllers (Meta Quest, Vive, Index, etc.).
<a-entity id="leftHand"
hand-controls="hand: left; handModelStyle: lowPoly; color: #ffcccc">
</a-entity>
<a-entity id="rightHand"
hand-controls="hand: right; handModelStyle: highPoly; color: #ffcccc">
</a-entity>| Property | Type | Default | Description |
|---|---|---|---|
hand | string | left | Which hand (left, right) |
handModelStyle | string | lowPoly | Model detail (lowPoly, highPoly, toon) |
color | color | white | Hand color |
Laser Controls
Add laser pointer to controllers for UI interaction.
<a-entity
hand-controls="hand: right"
laser-controls="hand: right"
raycaster="objects: .interactive; far: 10">
</a-entity>
<!-- Interactive object -->
<a-box class="interactive" position="0 1.5 -3"></a-box>Controller Events
const leftHand = document.querySelector('#leftHand');
const rightHand = document.querySelector('#rightHand');
// Trigger button
leftHand.addEventListener('triggerdown', (evt) => {
console.log('Left trigger pressed');
});
leftHand.addEventListener('triggerup', (evt) => {
console.log('Left trigger released');
});
// Grip button
rightHand.addEventListener('gripdown', (evt) => {
console.log('Right grip pressed');
});
rightHand.addEventListener('gripup', (evt) => {
console.log('Right grip released');
});
// Thumbstick/touchpad
rightHand.addEventListener('thumbstickmoved', (evt) => {
console.log('Thumbstick:', evt.detail.x, evt.detail.y);
});
rightHand.addEventListener('touchpadmoved', (evt) => {
console.log('Touchpad:', evt.detail.x, evt.detail.y);
});
// A/B/X/Y buttons
rightHand.addEventListener('abuttondown', () => {
console.log('A button pressed');
});
rightHand.addEventListener('bbuttondown', () => {
console.log('B button pressed');
});
leftHand.addEventListener('xbuttondown', () => {
console.log('X button pressed');
});
leftHand.addEventListener('ybuttondown', () => {
console.log('Y button pressed');
});Platform-Specific Controllers
Meta Quest / Oculus Touch
<a-entity meta-touch-controls="hand: left; model: true"></a-entity>
<a-entity meta-touch-controls="hand: right; model: true"></a-entity>HTC Vive
<a-entity vive-controls="hand: left; buttonColor: #FF0000"></a-entity>
<a-entity vive-controls="hand: right; buttonColor: #0000FF"></a-entity>Valve Index
<a-entity valve-index-controls="hand: left"></a-entity>
<a-entity valve-index-controls="hand: right"></a-entity>Windows Mixed Reality
<a-entity windows-motion-controls="hand: left"></a-entity>
<a-entity windows-motion-controls="hand: right"></a-entity>Grabbable Objects Component
AFRAME.registerComponent('grabbable', {
init: function() {
var el = this.el;
var grabbing = false;
var controller = null;
el.addEventListener('triggerdown', function(evt) {
if (!grabbing) {
grabbing = true;
controller = evt.detail.controller;
// Attach object to controller
controller.object3D.attach(el.object3D);
// Visual feedback
el.setAttribute('material', 'opacity', 0.7);
}
});
el.addEventListener('triggerup', function(evt) {
if (grabbing && controller === evt.detail.controller) {
grabbing = false;
// Detach from controller
var sceneEl = el.sceneEl.object3D;
sceneEl.attach(el.object3D);
// Reset visual feedback
el.setAttribute('material', 'opacity', 1);
controller = null;
}
});
}
});<!-- Apply to objects -->
<a-box class="grabbable" grabbable position="0 1.5 -3"></a-box>
<a-sphere class="grabbable" grabbable position="1 1.5 -3"></a-sphere>---
Hand Tracking
Native hand tracking without controllers (supported on Meta Quest 2/3/Pro).
Enable Hand Tracking
<a-scene webxr="requiredFeatures: hand-tracking">
<!-- Hand tracking entities -->
<a-entity id="leftHand"
hand-tracking-controls="hand: left"
hand-tracking-grab-controls="hand: left">
</a-entity>
<a-entity id="rightHand"
hand-tracking-controls="hand: right"
hand-tracking-grab-controls="hand: right">
</a-entity>
<!-- Grabbable objects -->
<a-box class="grabbable" position="0 1.5 -3"></a-box>
</a-scene>Hand Tracking Properties
<a-entity hand-tracking-controls="
hand: left;
modelColor: #FF0000;
modelStyle: mesh
"></a-entity>| Property | Type | Default | Description |
|---|---|---|---|
hand | string | left | Which hand (left, right) |
modelColor | color | white | Hand mesh color |
modelStyle | string | mesh | Visualization (mesh, dots, none) |
Hand Tracking Events
const leftHand = document.querySelector('#leftHand');
// Pinch gesture (thumb + index finger)
leftHand.addEventListener('pinchstarted', (evt) => {
console.log('Pinch started');
});
leftHand.addEventListener('pinchended', (evt) => {
console.log('Pinch ended');
});
// Pinch with strength value
leftHand.addEventListener('pinchmoved', (evt) => {
console.log('Pinch strength:', evt.detail.strength); // 0-1
});Hand Tracking Grab Controls
<a-scene>
<!-- Enable hand tracking grab -->
<a-entity
hand-tracking-controls="hand: right"
hand-tracking-grab-controls="hand: right">
</a-entity>
<!-- Grabbable object -->
<a-sphere
class="grabbable"
obb-collider="size: 0.2 0.2 0.2"
grab-options="
requireGrab: true;
maxGrabbers: 2
"
position="0 1.5 -3">
</a-sphere>
</a-scene>Visualize Hand Tracking Colliders (Debug)
<a-scene obb-collider="showColliders: true">
<!-- Shows bounding boxes for debugging -->
</a-scene>---
AR Hit Testing
Place virtual objects on detected real-world surfaces.
Basic AR Hit Test
<a-scene
webxr="optionalFeatures: hit-test"
ar-hit-test="target: #model">
<a-assets>
<a-asset-item id="furniture" src="chair.gltf"></a-asset-item>
</a-assets>
<a-entity id="model" gltf-model="#furniture" scale="0.5 0.5 0.5"></a-entity>
</a-scene>Custom Reticle
<a-scene
webxr="optionalFeatures: hit-test"
ar-hit-test="target: #model; src: #reticle">
<a-assets>
<a-asset-item id="reticle-model" src="reticle.gltf"></a-asset-item>
<a-asset-item id="furniture" src="chair.gltf"></a-asset-item>
</a-assets>
<!-- Custom reticle -->
<a-entity id="reticle" gltf-model="#reticle-model"></a-entity>
<!-- Object to place -->
<a-entity id="model" gltf-model="#furniture"></a-entity>
</a-scene>Multiple Object Placement
const scene = document.querySelector('a-scene');
const furniture = document.querySelector('#furniture');
let placedObjects = [];
scene.addEventListener('ar-hit-test-select', function(evt) {
// Clone object for multiple placements
const clone = furniture.cloneNode(true);
clone.removeAttribute('id');
clone.setAttribute('visible', true);
// Position at hit point
const hitPoint = evt.detail.position;
clone.setAttribute('position', hitPoint);
scene.appendChild(clone);
placedObjects.push(clone);
console.log('Placed object at:', hitPoint);
});
// Clear all placed objects
function clearObjects() {
placedObjects.forEach(obj => obj.parentNode.removeChild(obj));
placedObjects = [];
}AR with DOM Overlay
<a-scene
webxr="optionalFeatures: hit-test, dom-overlay; overlayElement: #overlay"
ar-hit-test="target: #model">
<a-entity id="model" gltf-model="#furniture"></a-entity>
<!-- HTML UI overlay -->
<div id="overlay" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;">
<div style="position: absolute; top: 20px; left: 50%; transform: translateX(-50%);
background: rgba(0,0,0,0.7); color: white; padding: 15px; border-radius: 8px; pointer-events: auto;">
<p id="instructions">Tap to enter AR</p>
<button id="clearBtn" style="margin-top: 10px; padding: 10px 20px; pointer-events: auto;">Clear Objects</button>
</div>
</div>
</a-scene>
<script>
const clearBtn = document.getElementById('clearBtn');
clearBtn.addEventListener('click', clearObjects);
</script>---
Platform Support
Meta Quest (Standalone)
Optimizations for Quest 2/3/Pro:
<a-scene
renderer="antialias: false; physicallyCorrectLights: false"
vr-mode-ui="enabled: true">
<!-- Use low-poly models -->
<a-entity gltf-model="#low-poly-model"></a-entity>
<!-- Limit lights (1-2 max) -->
<a-entity light="type: ambient; intensity: 0.7"></a-entity>
<a-entity light="type: directional; intensity: 0.3" position="1 2 1"></a-entity>
<!-- Texture size limits -->
<a-entity material="src: #texture; repeat: 1 1"></a-entity>
</a-scene>Desktop VR (PC + Headset)
Higher quality settings for PC VR:
<a-scene
renderer="antialias: true; colorManagement: true; physicallyCorrectLights: true"
fog="type: linear; color: #AAA; near: 10; far: 100">
<!-- High-poly models allowed -->
<a-entity gltf-model="#high-poly-model"></a-entity>
<!-- Multiple lights OK -->
<a-entity light="type: ambient; intensity: 0.5"></a-entity>
<a-entity light="type: directional; intensity: 0.8" position="2 4 2"></a-entity>
<a-entity light="type: point; intensity: 1.5; distance: 20" position="5 2 5"></a-entity>
</a-scene>Mobile AR (iOS/Android)
Optimizations for mobile AR:
<a-scene
webxr="optionalFeatures: hit-test, dom-overlay, light-estimation"
ar-hit-test="target: #model"
renderer="antialias: false; maxCanvasWidth: 1920; maxCanvasHeight: 1920">
<!-- Lightweight models for mobile -->
<a-entity gltf-model="#mobile-optimized-model"></a-entity>
<!-- Minimal lighting -->
<a-entity light="type: ambient; intensity: 0.8"></a-entity>
</a-scene>Feature Detection
// Check WebXR support
if ('xr' in navigator) {
navigator.xr.isSessionSupported('immersive-vr').then((supported) => {
if (supported) {
console.log('VR supported');
}
});
navigator.xr.isSessionSupported('immersive-ar').then((supported) => {
if (supported) {
console.log('AR supported');
}
});
}
// Check hand tracking support
const scene = document.querySelector('a-scene');
scene.addEventListener('loaded', () => {
if (scene.systems['hand-tracking-controls']) {
console.log('Hand tracking available');
}
});---
Performance Optimization
Reduce Draw Calls
// Use instancing for repeated objects
AFRAME.registerComponent('instanced-forest', {
init: function() {
const scene = this.el.sceneEl.object3D;
const geometry = new THREE.CylinderGeometry(0.2, 0.5, 3, 8);
const material = new THREE.MeshStandardMaterial({ color: 0x8B4513 });
const instancedMesh = new THREE.InstancedMesh(geometry, material, 100);
// Position instances
for (let i = 0; i < 100; i++) {
const matrix = new THREE.Matrix4();
matrix.setPosition(
Math.random() * 20 - 10,
0,
Math.random() * 20 - 10
);
instancedMesh.setMatrixAt(i, matrix);
}
scene.add(instancedMesh);
}
});Optimize Geometry
<!-- Low poly count for VR/mobile -->
<a-sphere radius="1" segments-width="8" segments-height="6"></a-sphere>
<!-- High poly only for close-up objects -->
<a-sphere radius="1" segments-width="32" segments-height="32"></a-sphere>Texture Optimization
// Compress textures
// Use power-of-2 sizes (256, 512, 1024, 2048)
// Use lower resolutions for mobile
// Lazy load textures
AFRAME.registerComponent('lazy-texture', {
schema: {
src: {type: 'string'}
},
init: function() {
const el = this.el;
const src = this.data.src;
// Load texture when entity is near camera
this.el.sceneEl.addEventListener('camera-move', () => {
const distance = el.object3D.position.distanceTo(
el.sceneEl.camera.position
);
if (distance < 10 && !el.getAttribute('material').src) {
el.setAttribute('material', 'src', src);
}
});
}
});Limit Physics
<!-- Only enable physics for interactive objects -->
<a-entity
geometry="primitive: box"
ammo-body="type: dynamic; mass: 1"
ammo-shape="type: box">
</a-entity>Throttle Updates
AFRAME.registerComponent('throttled-rotation', {
init: function() {
this.lastUpdate = 0;
this.updateInterval = 100; // Update every 100ms instead of every frame
},
tick: function(time, timeDelta) {
if (time - this.lastUpdate >= this.updateInterval) {
// Expensive operation
this.el.object3D.rotation.y += 0.01;
this.lastUpdate = time;
}
}
});---
Testing and Debugging
Desktop Testing
<!-- Test without VR headset using desktop mode -->
<a-scene vr-mode-ui="enabled: true">
<!-- WASD to move, mouse to look -->
<a-camera wasd-controls look-controls></a-camera>
</a-scene>Mobile Testing
<!-- Test AR on mobile browser -->
<a-scene
webxr="optionalFeatures: hit-test"
ar-hit-test="target: #model">
<!-- Use browser DevTools device emulation -->
</a-scene>Stats and Debugging
<!-- Show FPS and performance stats -->
<a-scene stats>
<!-- Stats panel appears in top-left -->
</a-scene>
<!-- Enable inspector (Ctrl+Alt+I) -->
<a-scene inspector>
<!-- Visual scene editor -->
</a-scene>Console Logging
// Log XR session info
const scene = document.querySelector('a-scene');
scene.addEventListener('enter-vr', () => {
const renderer = scene.renderer;
const session = renderer.xr.getSession();
console.log('XR Session:', session);
console.log('Reference space:', scene.systems.webxr.sessionReferenceSpaceType);
console.log('Frame rate:', session.frameRate);
});Remote Debugging
For Meta Quest: 1. Enable Developer Mode in Quest settings 2. Connect via USB to computer 3. Use Chrome DevTools (chrome://inspect)
For iOS: 1. Enable Web Inspector in Safari settings 2. Connect iPhone/iPad to Mac 3. Use Safari Developer menu
Performance Profiling
// Monitor frame times
const scene = document.querySelector('a-scene');
let frameCount = 0;
let lastTime = performance.now();
scene.addEventListener('renderstart', () => {
const now = performance.now();
frameCount++;
if (now - lastTime >= 1000) {
console.log('FPS:', frameCount);
frameCount = 0;
lastTime = now;
}
});---
Common Issues
Issue: VR button not appearing
Solution: Check HTTPS (required for WebXR)
Issue: Controllers not tracking
Solution: Check permissions, ensure proper lighting
Issue: AR not working on mobile
Solution: Use Chrome/Safari, check camera permissions
Issue: Low FPS in VR
Solution: Reduce geometry, limit lights, optimize textures
Issue: Hand tracking not working
Solution: Enable in headset settings, ensure good lighting
---
Resources
- WebXR Device API Specification
- A-Frame WebXR Documentation
- Meta Quest Development
- WebXR Samples
- Mozilla Mixed Reality Blog
---
This guide covers all aspects of building WebXR experiences with A-Frame, from basic VR scenes to advanced AR features.
Related skills
How it compares
Pick aframe-webxr over raw Three.js when you want HTML-first WebXR prototyping with entity-component composition and less boilerplate.
FAQ
What does aframe-webxr do?
Declarative web framework for building browser-based 3D, VR, and AR experiences using HTML and entity-component architecture. Use this skill when creating WebXR applications, VR experiences, AR experiences, 360-degree me
When should I use aframe-webxr?
Declarative web framework for building browser-based 3D, VR, and AR experiences using HTML and entity-component architecture. Use this skill when creating WebXR applications, VR experiences, AR experiences, 360-degree me
What are common prerequisites?
--- name: aframe-webxr description: Declarative web framework for building browser-based 3D, VR, and AR experiences using HTML and entity-component architecture.
Is Aframe Webxr safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.