Command Palette

Search for a command to run...

Hectal
Course outline
Topic 18.3Advanced ~20 min · step 3 of 7

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.

0/7 · 0%

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, or aria-label and aria-describedby.
live region
An element whose changed text a screen reader says aloud without moving focus: aria-live or role="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. 1

    Fix the grey

    In the contrast example change #6b7686 to #8a94a3 (in both places). The last line becomes 5.52 needs 4.5 pass. #9aa4b2 gives 6.72.

  2. 2

    Add a fifth part

    In the first example add "bottle-cage": ["Bottle cage", 18] to PARTS. 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

A text alternative, real buttons and an announcement New tab

`.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.

idle

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: 1
Contrast ratios, computed New tab

Plain 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 FAIL

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

terminal
$ Run
── what you'll see ──
announced: Front wheel selected. 120 euros. | pressed: 0
announced: Rear wheel selected. 130 euros. | pressed: 0
announced: Handlebar and stem selected. 60 euros. | pressed: 0
announced: Bell selected. 12 euros. | pressed: 0

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 #live does.

  • ▸

    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. 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. 2

    A text alternative. Give the container an aria-label and an aria-describedby that 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 put role="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. 3

    The parts list is the real interface. Phase 8's panel already has one real <button> per part with aria-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. 4

    Announcements. A live region (role="status" or aria-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. 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. 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. Its A11y component makes a 3D object focusable with a role and description; 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

01

A screen-reader user opens a page with only a full-screen canvas. What do they get, and what three things would you add?

02

How do you make a live region announce the same message twice?

Practice

01

Write announce(message) that works for repeated messages.

02

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.