Course outline
Accessibility of 3D
In one line
To a screen reader a canvas is one picture with nothing inside. Give it a text alternative, offer every part as a real button, announce changes politely, keep focus visible and check contrast with numbers.
Think of it like this
A museum has a ship model behind glass. A visitor who cannot see it needs a card that says what it is and a guide who says what changes when a handle turns. The canvas is the glass case, the text alternative the card, the announcement the guide's voice.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- screen reader
- Software that reads a page aloud (or to a braille display) from its structure: headings, buttons, labels.
- text alternative
- Words that say what a picture shows, for people who cannot see it. In HTML:
alt, oraria-labelandaria-describedby. - live region
- An element whose changed text a screen reader says aloud without moving focus:
aria-liveorrole="status". - contrast ratio
- How different two colours' brightness is, from 1:1 (same) to 21:1 (black on white). WCAG asks 4.5:1 for text, 3:1 for control edges.
Step by step
01Write the description from the scene
The first example collects the meshes in the scene and builds one sentence: "An orange Lumen bike on a round stand, turning slowly. It has 4 parts you can choose: front wheel, rear wheel, handlebar and stem, bell." Add a part to the scene and the sentence changes by itself.
02Real buttons, a live region
Each part gets a real <button> with aria-pressed. Choosing one sets the hidden #live paragraph, which is role="status". The example presses each button like a keyboard user would and prints what the live region then holds: "Front wheel selected. 120 euros."
03Know what to aim for
WCAG asks 4.5:1 for text (3:1 for large text) and 3:1 for control edges and focus rings.
04Compute the contrast
The second example turns hex colours into linear-light luminance and prints six pairs from the showroom's CSS, plus a grey #6b7686 that fails at 3.68:1 (unused). The worst real case, panel text over the bright stage, is 12.26:1.
Try it yourself
- 1
Fix the grey
In the contrast example change
#6b7686to#8a94a3(in both places). The last line becomes5.52 needs 4.5 pass.#9aa4b2gives6.72. - 2
Add a fifth part
In the first example add
"bottle-cage": ["Bottle cage", 18]toPARTS. The sentence now says "It has 5 parts ..." and ends with "..., bell, bottle cage." It is written from the data, so it cannot go out of date.
Code & diagrams
`.sr-only` text is read by a screen reader and invisible on screen. The buttons are pressed from code, like a keyboard user pressing Enter.
Console
Run the scene to see its console.Expected console output
alt: An orange Lumen bike on a round stand, turning slowly. It has 4 parts you can choose: front wheel, rear wheel, handlebar and stem, bell.
announced: Front wheel selected. 120 euros. | pressed: 1
announced: Rear wheel selected. 130 euros. | pressed: 1
announced: Handlebar and stem selected. 60 euros. | pressed: 1
announced: Bell selected. 12 euros. | pressed: 1Plain JavaScript, no 3D: contrast is arithmetic, so check it in code and in CI.
Console
(nothing was printed)Expected console output
panel text on the panel, over black 14.66 needs 4.5 pass
panel text on the panel, over the bright stage 12.26 needs 4.5 pass
chosen button: dark text on orange 6.67 needs 4.5 pass
focus ring on the dark page 6.53 needs 3 pass
focus ring on the panel 5.83 needs 3 pass
orange text on the panel 5.96 needs 4.5 pass
grey hint text #6b7686 on the panel 3.68 needs 4.5 FAILBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Forget aria-pressed
In the first example delete the line for (const other of list.children) other.setAttribute("aria-pressed", String(other === button));.
Myth vs fact
Myth
A 3D scene cannot be made accessible.
Fact
The picture cannot be read, but everything the picture tells can also be said in text and offered as buttons. The showroom's parts list does exactly that.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Live regions are fragile: a region inserted into the page and filled in the same moment is often not read, because the screen reader starts watching it after it exists. Keep one in the HTML from the start and change only its text, as
#livedoes. - ▸
Contrast over a moving 3D picture is not one number: give overlays a near-solid background (the panel is 93 % opaque) and test against the brightest colour behind it, as the example does with
#d9dee7.
Remember this
- 1
The problem this solves: the browser can read a page's buttons, headings and text to a screen reader. It cannot read what is drawn in a
<canvas>: the pixels are one bitmap and no objects exist inside it. If the 3D view is the only way to learn something, a person who cannot see it learns nothing. Ravi, in the story, heard "group" and then silence. - 2
A text alternative. Give the container an
aria-labeland anaria-describedbythat points to a visually hidden paragraph. Write the paragraph from the scene itself, so it never lists a part that is not there. Do not putrole="img"on a container that holds real buttons: it hides everything inside from assistive technology. The first example builds the sentence from the scene's parts. - 3
The parts list is the real interface. Phase 8's panel already has one real
<button>per part witharia-pressed. A screen-reader or keyboard user can pick the bell without seeing it. Keep the buttons after the viewer in page order, and the keyboard twins of every pointer action (Topic 8.4). Phase 9's Pause button and reduced-motion rule (Topic 9.5) belong here too: a visitor must be able to stop movement. - 4
Announcements. A live region (
role="status"oraria-live="polite") is read aloud when its text changes, without moving focus. The same text set twice is not read twice, so clear it first. Use it for what the visitor cannot see: "The Lumen bike is ready", "Bell selected, 12 euros", "The 3D view was interrupted". - 5
Contrast with numbers. WCAG asks for a contrast ratio of at least 4.5:1 for text and 3:1 for the parts of controls and focus indicators. The ratio is (lighter + 0.05) / (darker + 0.05) on linear-light luminance. The overlay panel is 93 % opaque, so the colour behind the text depends on the 3D picture. The second example computes the real ratios, including the worst case, over the bright stage.
- 6
**React and
@react-three/a11y.** On npm (checked 2026-10-18) the latest version is 3.0.0 from 2022-05-15, with no deprecation notice. ItsA11ycomponent makes a 3D object focusable with aroleanddescription; you add the focus visuals yourself. It has had no release for over four years and we did not run it, so test it with a real screen reader.
Explain it without notes
A screen-reader user opens a page with only a full-screen canvas. What do they get, and what three things would you add?
How do you make a live region announce the same message twice?
Practice
Write announce(message) that works for repeated messages.
A button's text is #ff6b2c on #0b1118. Without running code, is it above 4.5:1? Then check with the example.
Done when you can
You can give a canvas a text alternative written from the scene.
You offer every part as a real button with its state, and keep keyboard twins.
You can announce a change with a live region without moving focus.
You can compute a contrast ratio and name the two WCAG thresholds.