Three.js From Zero · Article s7-07
S7-07 Accessibility in 3D
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.