Three.js From Zero · Article s7-07

S7-07 Accessibility in 3D

Season 7 · Article 07

Accessibility in 3D Apps

Canvas is a black box to screen readers. 3D needs extra work for keyboard nav, reduced motion, color-blindness, and focus states. Here's how to make your Three.js app pass WCAG.

1. The fundamental challenge

Your canvas is a single <canvas> element. To a screen reader, it's "graphic". All the 3D content is invisible.

To fix: provide a parallel HTML representation. Invisible to sighted users, meaningful to assistive tech.

2. Alternative content pattern

<div class="canvas-wrapper" role="application" aria-label="3D product viewer">
  <canvas></canvas>

  <div class="sr-only">
    <h2>Product: Red Leather Chair</h2>
    <ul>
      <li>Material: top-grain leather</li>
      <li>Dimensions: 30 × 28 × 36 inches</li>
      <li>Color variants: Red, Black, Brown</li>
    </ul>
    <button>Rotate left</button>
    <button>Rotate right</button>
    <button>Zoom in</button>
    <button>Zoom out</button>
    <button>Change color to Black</button>
  </div>
</div>

Screen reader reads the structure. Each button performs the same action as the visual gesture would.

3. Keyboard nav

document.addEventListener('keydown', e => {
  switch (e.key) {
    case 'ArrowLeft':  rotateCamera(-0.1); break;
    case 'ArrowRight': rotateCamera(0.1); break;
    case '+': zoom(1.1); break;
    case '-': zoom(0.9); break;
    case 'Tab':  focusNextHotspot(); break;
    case 'Enter': activateHotspot(); break;
  }
});

Arrow keys orbit. +/- zoom. Tab/Shift+Tab cycle through interactive 3D objects. Enter activates focused one.

4. Focus indicators for 3D objects

When Tab focuses a 3D hotspot, render an outline:

import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js';
const outline = new OutlinePass(new THREE.Vector2(w, h), scene, camera);
outline.edgeStrength = 5;
composer.addPass(outline);

function focusObject(obj) {
  outline.selectedObjects = [obj];
  // Also speak the focus change
  announce('Focused: ' + obj.userData.name);
}

5. Live region for dynamic announcements

<div aria-live="polite" aria-atomic="true" class="sr-only" id="announcer"></div>

function announce(text) {
  document.getElementById('announcer').textContent = text;
}

// Use when state changes the user can't visually track
announce('Rotating to view from above');
announce('Color changed to Black');

6. Reduced motion

const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reducedMotion) {
  controls.autoRotate = false;
  animation.duration *= 0.1;  // or disable
}

Respect this preference religiously. Spinning 3D scenes trigger vestibular disorders.

7. Color blindness

  • Don't encode information via color alone. Add shape, pattern, label.
  • Test with Coblis.
  • WCAG AA: 4.5:1 contrast for text. Applies to label text over 3D backgrounds.

8. High contrast mode

@media (prefers-contrast: more) {
  .hud { border: 2px solid white; background: black; }
}

// In Three.js
if (matchMedia('(prefers-contrast: more)').matches) {
  scene.background = new THREE.Color(0x000000);
  material.color.set(0xffffff);
}

9. Live demo — keyboard-accessible product viewer

Tab through hotspots. Arrow keys rotate. + / - zoom. Announcements via live region.

Focus the demo (click), then use arrow keys, +/-, or Tab.

10. Takeaways

  • Canvas = invisible to screen readers. Provide HTML mirror.
  • Keyboard: arrows for camera, Tab for hotspots, Enter activates.
  • Outline pass shows focused 3D object.
  • aria-live regions announce state changes.
  • Respect prefers-reduced-motion and prefers-contrast.
  • Never encode info by color alone.