or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

accessibility.mdanimation.mdannotation.mdar.mdcontrols.mdenvironment.mdindex.mdloading.mdscene-graph.md
tile.json

accessibility.mddocs/

Accessibility

Accessibility features and structured data generation to ensure the 3D viewer is usable by all users and properly indexed by search engines.

Capabilities

Alternative Text

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);

Structured Data Generation

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"
    }
  ]
}

Keyboard Navigation

Model Viewer provides built-in keyboard navigation support when camera controls are enabled.

Default Keyboard Controls

// 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'
};

Custom Keyboard Handlers

// 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');
}

Screen Reader Support

Enhance screen reader accessibility with announcements and descriptions.

Dynamic Announcements

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);

Descriptive Labels

// 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();
      }
    });
  });
}

High Contrast and Reduced Motion

Support users with visual impairments and motion sensitivity.

High Contrast Mode

/* 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;
  }
}

Reduced Motion Support

// 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);

Focus Management

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);

Complete Accessibility Example

<!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>