Accessibility features and structured data generation to ensure the 3D viewer is usable by all users and properly indexed by search engines.
Provide descriptive text for screen readers and accessibility tools.
/**
* Alternative text description of the 3D model
* Used by screen readers and accessibility tools
* Should describe the model's appearance and purpose
*/
alt: string;Usage:
<!-- Descriptive alt text for screen readers -->
<model-viewer
src="models/red-sports-car.glb"
alt="Red sports car with black interior, convertible top down, facing forward">
</model-viewer>
<!-- Product description -->
<model-viewer
src="models/office-chair.glb"
alt="Modern ergonomic office chair in black leather with adjustable height and armrests">
</model-viewer>// Update alt text dynamically based on model state
function updateAltText() {
const variant = modelViewer.variantName || 'default';
const baseDescription = 'Modern office chair with adjustable height';
const variantDescriptions = {
'black_leather': 'in black leather with chrome accents',
'gray_fabric': 'in gray fabric upholstery',
'white_mesh': 'with white mesh back and cushioned seat'
};
const variantText = variantDescriptions[variant] || '';
modelViewer.alt = `${baseDescription} ${variantText}`;
}
// Update alt text when variant changes
modelViewer.addEventListener('variant-change', updateAltText);Generate JSON-LD structured data for search engine optimization.
/**
* Enable automatic generation of structured data (Schema.org)
* Creates JSON-LD metadata for better SEO and rich snippets
*/
generateSchema: boolean;
/**
* Speed of auto-rotation in degrees per second
* Set to 'auto' for default speed, or specific value like '30deg'
*/
rotationPerSecond: string;Usage:
<!-- Enable structured data generation -->
<model-viewer
src="models/product.glb"
alt="Wireless headphones in black"
generate-schema>
</model-viewer>The generated structured data follows Schema.org format:
{
"@context": "https://schema.org",
"@type": "3DModel",
"name": "Product Model",
"description": "Wireless headphones in black",
"image": "poster-image-url",
"encoding": [
{
"@type": "MediaObject",
"contentUrl": "models/product.glb",
"encodingFormat": "model/gltf-binary"
}
]
}Model Viewer provides built-in keyboard navigation support when camera controls are enabled.
// Keyboard navigation (automatically enabled with camera-controls)
const keyboardControls = {
'ArrowLeft': 'Orbit left',
'ArrowRight': 'Orbit right',
'ArrowUp': 'Orbit up',
'ArrowDown': 'Orbit down',
'PageUp': 'Zoom in',
'PageDown': 'Zoom out',
'Home': 'Reset camera to initial position'
};// Add custom keyboard navigation
function setupCustomKeyboard() {
modelViewer.addEventListener('keydown', (event) => {
const step = 5; // degrees
const currentOrbit = modelViewer.getCameraOrbit();
switch (event.code) {
case 'KeyW': // W key - move up
event.preventDefault();
modelViewer.cameraOrbit = `${currentOrbit.theta}rad ${currentOrbit.phi - step * Math.PI/180}rad ${currentOrbit.radius}`;
break;
case 'KeyS': // S key - move down
event.preventDefault();
modelViewer.cameraOrbit = `${currentOrbit.theta}rad ${currentOrbit.phi + step * Math.PI/180}rad ${currentOrbit.radius}`;
break;
case 'KeyA': // A key - orbit left
event.preventDefault();
modelViewer.cameraOrbit = `${currentOrbit.theta - step * Math.PI/180}rad ${currentOrbit.phi}rad ${currentOrbit.radius}`;
break;
case 'KeyD': // D key - orbit right
event.preventDefault();
modelViewer.cameraOrbit = `${currentOrbit.theta + step * Math.PI/180}rad ${currentOrbit.phi}rad ${currentOrbit.radius}`;
break;
case 'KeyR': // R key - reset camera
event.preventDefault();
modelViewer.cameraOrbit = '0deg 75deg 105%';
modelViewer.cameraTarget = 'auto auto auto';
break;
}
});
// Ensure model viewer can receive focus
modelViewer.tabIndex = 0;
modelViewer.setAttribute('role', 'img');
}Enhance screen reader accessibility with announcements and descriptions.
class AccessibilityManager {
constructor(modelViewer) {
this.modelViewer = modelViewer;
this.announcer = this.createAnnouncer();
this.setupEventListeners();
}
createAnnouncer() {
const announcer = document.createElement('div');
announcer.setAttribute('role', 'status');
announcer.setAttribute('aria-live', 'polite');
announcer.setAttribute('aria-atomic', 'true');
announcer.style.position = 'absolute';
announcer.style.left = '-10000px';
announcer.style.width = '1px';
announcer.style.height = '1px';
announcer.style.overflow = 'hidden';
document.body.appendChild(announcer);
return announcer;
}
announce(message) {
this.announcer.textContent = message;
// Clear after announcement
setTimeout(() => {
this.announcer.textContent = '';
}, 1000);
}
setupEventListeners() {
// Announce model loading states
this.modelViewer.addEventListener('progress', (event) => {
const progress = Math.round(event.detail.totalProgress * 100);
if (progress % 25 === 0) { // Announce every 25%
this.announce(`Model loading ${progress}% complete`);
}
});
this.modelViewer.addEventListener('load', () => {
this.announce('3D model loaded successfully');
});
this.modelViewer.addEventListener('error', () => {
this.announce('Error loading 3D model');
});
// Announce animation changes
this.modelViewer.addEventListener('animation-change', () => {
const animationName = this.modelViewer.animationName;
if (animationName) {
this.announce(`Animation changed to ${animationName}`);
}
});
// Announce AR mode changes
this.modelViewer.addEventListener('ar-status', (event) => {
const status = event.detail.status;
const messages = {
'session-started': 'AR mode activated',
'object-placed': 'Model placed in AR space',
'not-presenting': 'AR mode deactivated',
'failed': 'AR mode failed to start'
};
if (messages[status]) {
this.announce(messages[status]);
}
});
}
}
// Initialize accessibility manager
const accessibilityManager = new AccessibilityManager(modelViewer);// Add descriptive labels for interactive elements
function setupAccessibilityLabels() {
// Label the model viewer itself
modelViewer.setAttribute('role', 'img');
modelViewer.setAttribute('aria-label', modelViewer.alt || 'Interactive 3D model');
// Add labels to hotspots
const hotspots = modelViewer.querySelectorAll('[slot^="hotspot"]');
hotspots.forEach((hotspot, index) => {
if (!hotspot.getAttribute('aria-label')) {
hotspot.setAttribute('aria-label', `Hotspot ${index + 1}: Additional information`);
}
hotspot.setAttribute('role', 'button');
hotspot.setAttribute('tabindex', '0');
// Add keyboard activation
hotspot.addEventListener('keydown', (event) => {
if (event.code === 'Enter' || event.code === 'Space') {
event.preventDefault();
hotspot.click();
}
});
});
}Support users with visual impairments and motion sensitivity.
/* High contrast styles */
@media (prefers-contrast: high) {
model-viewer {
outline: 3px solid black;
}
.hotspot {
background: black;
color: white;
border: 2px solid white;
}
.hotspot-trigger {
background: white;
border: 2px solid black;
}
}// Respect user's motion preferences
function setupMotionPreferences() {
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReducedMotion) {
// Disable auto-rotation
modelViewer.autoRotate = false;
// Reduce animation speeds
modelViewer.style.setProperty('--interaction-prompt-duration', '0s');
// Disable camera animations
modelViewer.interpolationDecay = 1000; // Very fast, almost instant
// Disable auto-play animations
modelViewer.autoplay = false;
console.log('Reduced motion preferences detected - animations minimized');
}
}
// Apply on load and when preferences change
setupMotionPreferences();
window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', setupMotionPreferences);Proper focus management for keyboard navigation and screen readers.
class FocusManager {
constructor(modelViewer) {
this.modelViewer = modelViewer;
this.setupFocusManagement();
}
setupFocusManagement() {
// Make model viewer focusable
this.modelViewer.tabIndex = 0;
// Focus styles
this.modelViewer.addEventListener('focus', () => {
this.modelViewer.style.outline = '3px solid #4A90E2';
this.announceInstructions();
});
this.modelViewer.addEventListener('blur', () => {
this.modelViewer.style.outline = 'none';
});
// Trap focus during AR mode
this.modelViewer.addEventListener('ar-status', (event) => {
if (event.detail.status === 'session-started') {
this.trapFocus();
} else if (event.detail.status === 'not-presenting') {
this.releaseFocus();
}
});
}
announceInstructions() {
const instructions = this.modelViewer.cameraControls ?
'Use arrow keys to rotate the model, Page Up and Page Down to zoom, Home to reset' :
'Interactive 3D model. Camera controls are disabled.';
setTimeout(() => {
this.announce(instructions);
}, 100);
}
trapFocus() {
// Store previously focused element
this.lastFocused = document.activeElement;
// Focus the model viewer
this.modelViewer.focus();
// Prevent tabbing away
document.addEventListener('keydown', this.handleFocusTrap);
}
releaseFocus() {
document.removeEventListener('keydown', this.handleFocusTrap);
// Restore focus to previously focused element
if (this.lastFocused) {
this.lastFocused.focus();
}
}
handleFocusTrap = (event) => {
if (event.code === 'Tab') {
event.preventDefault();
// Keep focus on model viewer during AR
this.modelViewer.focus();
}
}
}
// Initialize focus management
const focusManager = new FocusManager(modelViewer);<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accessible 3D Product Viewer</title>
<style>
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
@media (prefers-contrast: high) {
.hotspot {
background: black;
color: white;
border: 2px solid white;
}
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
</style>
</head>
<body>
<main>
<h1>Ergonomic Office Chair</h1>
<model-viewer
src="models/office-chair.glb"
alt="Modern ergonomic office chair in black leather with adjustable height, lumbar support, and chrome base"
camera-controls
touch-action="pan-y"
generate-schema
aria-label="Interactive 3D model of an office chair. Use arrow keys to rotate, Page Up/Down to zoom."
role="img">
<!-- Accessible hotspot -->
<div
slot="hotspot-features"
class="hotspot"
role="button"
tabindex="0"
aria-label="View chair features and specifications">
<span class="sr-only">Chair features: </span>
Ergonomic Features
</div>
<!-- Fallback content for non-supporting browsers -->
<div slot="poster">
<img src="images/chair-poster.jpg"
alt="Office chair product image"
style="width: 100%; height: 100%; object-fit: cover;">
</div>
<div slot="error">
<img src="images/chair-fallback.jpg"
alt="Office chair - 3D model not supported">
<p>3D model not supported in your browser.
<a href="images/chair-gallery.html">View image gallery</a>
</p>
</div>
</model-viewer>
<!-- Alternative content for screen readers -->
<div class="sr-only">
<h2>Product Description</h2>
<p>Modern ergonomic office chair featuring adjustable height,
lumbar support, padded armrests, and a sturdy chrome base with wheels.</p>
<h3>Key Features</h3>
<ul>
<li>360-degree swivel</li>
<li>Height adjustable from 18" to 22"</li>
<li>Lumbar support system</li>
<li>Breathable mesh back</li>
<li>Cushioned seat</li>
</ul>
</div>
</main>
</body>
</html>