<head> of your HTML file. This allows your web app to support upcoming discovery surfaces and enables us to message users when a website isn’t compatible with MRBD.<head> <!-- A brief description of your app --> <meta name="description" content="Description of your web app"> <!-- Identify your web app as MRBD-compatible --> <meta name="mrbd-web-app-capable" content="yes"> </head>
| Capability | Description and guidance |
|---|---|
Display | Additive waveguide overlay. Use dark backgrounds/light, high-contrast UI colors. Fixed 600x600px viewport. Avoid scrolling. |
Input | Navigation via Neural Band/captouch gestures translates to standard arrow key and Enter events. No mouse/touch/keyboard. All elements must be focusable. |
Sensors (IMU) | Standard DeviceMotionEvent (accelerometer, gyroscope) and DeviceOrientationEvent (heading, tilt, roll) W3C APIs. Requires user permission. |
Location (GPS) | Standard navigator.geolocation W3C API. Location is fetched from the paired mobile device. Requires user permission. |
Local Storage | Standard Web Storage APIs ( localStorage, sessionStorage). Best for lightweight data (preferences, small caches). Use JSON for structured data. |
App Icons | Use Unicode symbols or high-resolution PNG favicons (>= 52x52 px) via <link> tags or Web App Manifest. SVGs are not supported. |
meta tag to lock the scale and prevent unexpected zooming:<!-- optional --> <meta name="viewport" content="width=600, height=600, initial-scale=1.0, user-scalable=no">
overflow: hidden on the <body> element to ensure no content extends beyond the viewport boundary:body {
width: 600px;
height: 600px;
overflow: hidden;
}
ArrowUp, ArrowDown, ArrowLeft, ArrowRight) and Enter events delivered to your Web App.// — Input Constants —
const DPAD = {
UP: 'ArrowUp', DOWN: 'ArrowDown',
LEFT: 'ArrowLeft', RIGHT: 'ArrowRight',
SELECT: 'Enter', BACK: 'Escape',
};
// — Focus Management —
function moveFocus(direction) {
var focusables = Array.from(
document.querySelectorAll('.focusable:not([disabled]):not(.hidden)')
);
if (!focusables.length) return;
var idx = focusables.indexOf(document.activeElement);
if (idx === -1) { focusables[0].focus(); return; }
var next = (direction === 'up' || direction === 'left')
? (idx > 0 ? idx - 1 : focusables.length - 1)
: (idx < focusables.length - 1 ? idx + 1 : 0);
focusables[next].focus();
focusables[next].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
// — D-pad Listener —
document.addEventListener('keydown', function(e) {
switch (e.key) {
case DPAD.UP: moveFocus('up'); break;
case DPAD.DOWN: moveFocus('down'); break;
case DPAD.LEFT: moveFocus('left'); break;
case DPAD.RIGHT: moveFocus('right'); break;
case DPAD.SELECT:
if (document.activeElement.classList.contains('focusable')) {
document.activeElement.click();
}
break;
case DPAD.BACK: history.back(); break;
default: return; // don't preventDefault on unhandled keys
}
e.preventDefault();
});
<!-- Mark interactive elements with the focusable class --> <button class="focusable" data-action="settings">Settings</button> <button class="focusable" data-action="start">Start</button>
.focusable {
transition: all 150ms ease;
border: 2px solid transparent;
min-height: 88px; /* glasses minimum tap target */
}
.focusable:focus {
outline: none;
border-color: #00d4ff;
box-shadow: 0 0 20px rgba(0, 212, 255, 0.4);
}
DeviceMotionEvent and DeviceOrientationEvent web APIs. Simply add event listeners on window as you would in any mobile browser.DeviceOrientationEvent.requestPermission() exists and call it before attaching listeners.function startIMU() {
window.addEventListener('deviceorientation', handleOrientation);
window.addEventListener('devicemotion', handleMotion);
}
// Check whether requestPermission exists before calling it
if (typeof DeviceOrientationEvent !== 'undefined' &&
typeof DeviceOrientationEvent.requestPermission === 'function') {
// Platforms that require explicit permission (e.g., iOS Safari)
DeviceOrientationEvent.requestPermission()
.then(function(state) {
if (state === 'granted') {
startIMU();
}
});
} else {
// Glasses runtime and most Android browsers grant automatically
startIMU();
}
DeviceMotionEvent provides real-time accelerometer and gyroscope readings. Use it to detect movement, measure G-forces, or track rotation speed.window.addEventListener('devicemotion', function(e) {
// Accelerometer (including gravity), in m/s²
var ax = e.accelerationIncludingGravity.x;
var ay = e.accelerationIncludingGravity.y;
var az = e.accelerationIncludingGravity.z;
// Compute magnitude in G-force
var g = Math.sqrt(ax * ax + ay * ay + az * az) / 9.81;
document.getElementById('gforce').textContent = g.toFixed(2) + ' G';
// Gyroscope (rotation rate in degrees/second)
var yawRate = e.rotationRate.alpha;
var pitchRate = e.rotationRate.beta;
var rollRate = e.rotationRate.gamma;
});
DeviceOrientationEvent provides the current orientation of the glasses relative to the Earth. Use it for compass heading, tilt detection, or spatial UI.window.addEventListener('deviceorientation', function(e) {
var heading = e.alpha; // Compass direction (rotation around z-axis): 0-360°
var tilt = e.beta; // Forward/back tilt (rotation around x-axis): -180° to 180°
var roll = e.gamma; // Left/right tilt (rotation around y-axis): -90° to 90°
document.getElementById('heading').textContent = heading.toFixed(1) + '°';
});
| Consider | Avoid |
|---|---|
Requesting permission from a user gesture (for example, button press) | Calling requestPermission() automatically on page load |
Checking for API availability before attaching listeners | Assuming DeviceOrientationEvent is always defined |
Throttling or debouncing high-frequency sensor updates for UI rendering | Updating the DOM on every single sensor event without throttling |
Using accelerationIncludingGravity for tilt-based interactions | Relying on acceleration alone when gravity context is needed |
Removing event listeners when sensor data is no longer needed | Leaving listeners active in the background, which drains battery |
navigator.geolocation web API. Location data is fetched from the wearer’s paired mobile device, since the glasses themselves do not have location-aware sensors. Use the API exactly as you would in any Web App. Like Sensor Data, Location also requires user permission.getCurrentPosition to request a single location fix.navigator.geolocation.getCurrentPosition(
function(position) {
var coords = position.coords;
console.log('Latitude: ' + coords.latitude); // Decimal degrees
console.log('Longitude: ' + coords.longitude); // Decimal degrees
console.log('Accuracy: ' + coords.accuracy); // m
console.log('Altitude: ' + coords.altitude); // m (may be null)
console.log('Speed: ' + coords.speed); // m/s (may be null)
console.log('Heading: ' + coords.heading); // Degrees from north (may be null)
console.log('Timestamp: ' + position.timestamp); // ms since epoch (UTC)
},
function(error) {
// error.code:
// 1 = PERMISSION_DENIED: wearer denied permission request
// 2 = POSITION_UNAVAILABLE: location could not be retrieved (i.e., phone offline)
// 3 = TIMEOUT: request exceeded the timeout
// error.message - human-readable description
console.error('Geolocation error:', error.code, error.message);
},
{ timeout: 15000 }
);
watchPosition to receive ongoing location updates as the wearer moves.var watchId = navigator.geolocation.watchPosition(
function(position) {
// Called each time the position updates
updateMap(position.coords.latitude, position.coords.longitude);
},
function(error) {
// error.code:
// 1 = PERMISSION_DENIED: wearer denied permission request
// 2 = POSITION_UNAVAILABLE: location could not be retrieved (i.e., phone offline)
// 3 = TIMEOUT: request exceeded the timeout
// error.message - human-readable description
console.error('Watch error:', error.code, error.message);
}
);
clearWatch when updates are no longer needed.// Stop watching when done navigator.geolocation.clearWatch(watchId);
getCurrentPosition and watchPosition accept an optional third argument to configure behavior:| Option | Type | Default | Description |
|---|---|---|---|
enableHighAccuracy | boolean | false | Request the most accurate position available. May take longer and use more power. |
timeout | number | Infinity | Maximum time (in milliseconds) to wait for a position. Use 10000-15000 ms as a practical default. |
maximumAge | number | 0 | Accept a cached position if it is no older than this value (in milliseconds). |
navigator.geolocation.getCurrentPosition(successCb, errorCb, {
enableHighAccuracy: true, // Request most accurate position (boolean, default false)
timeout: 15000, // Max wait time in ms (number, default Infinity)
maximumAge: 5000 // Accept cached position if newer than this in ms (number, default 0)
});
| Consider | Avoid |
|---|---|
Using a timeout of 10-15 seconds (10000-15000 ms), since the first request may take several seconds | Omitting using a timeout or setting it too low |
Handling PERMISSION_DENIED gracefully, since the wearer must grant permission | Assuming wearer does not need to grant permissions |
Ensuring permissions requests are triggered by a user gesture | Assuming permission requests are triggered |
| Description | Error code | Type |
|---|---|---|
Wearer denies the permission prompt | 1 | PERMISSION_DENIED |
Location data could not be retrieved (for example, companion device is offline) | 2 | POSITION_UNAVAILABLE |
Request exceeded the specified timeout | 3 | TIMEOUT |
localStorage and sessionStorage, to persist lightweight data on MRBD glasses:localStorage persists data across sessions, even after the app is closed and reopened.sessionStorage persists data only for the current session, so values are cleared when the session ends.setItem, getItem, and removeItem methods.// Save a value
localStorage.setItem('userPreference', 'dark');
// Read a value
var preference = localStorage.getItem('userPreference');
// Returns 'dark', or null if the key does not exist
// Remove a value
localStorage.removeItem('userPreference');
// Clear all stored data
localStorage.clear();
sessionStorage has an identical API, but scopes data to the current session. Use it for temporary states that should not persist after the user exits your app.// Track whether the user has seen the onboarding screen this session
if (sessionStorage.getItem('onboardingSeen')) {
showMainScreen();
} else {
showOnboarding();
sessionStorage.setItem('onboardingSeen', 'true');
}
localStorage: 5 MBsessionStorage: 5 MBfavicon.ico) for this content. If no suitable icon is found, a default fallback icon is shown. SVGs are not supported.<link> tags in your page’s <head> section.<link rel="icon" href="/icon-96.png" sizes="96x96"> <link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
icons array must include a src attribute and ideal sizes.<link rel="manifest" href="/manifest.webmanifest">
{
"icons": [
{ "src": "/icons/icon-96.png", "sizes": "96x96" },
{ "src": "/icons/icon-192.png", "sizes": "192x192" }
]
}