# Accessibility of a configurator page

> Contract 2.0.0 · package sha256 `a857eca95b35921109faf997094bb4192f12527d9d0fcd4be7e97f06425bad6e` · generated from spec section 5.6 and `accessibilityPage` by `services/onsite-scripter/scripts/build-contract-docs.mjs`; edit the source, not this file.

What the runtime does: its badges are `<button>`s named by their `label`, in the Tab order in array order, drawn while focused even when hidden, and fired by Enter or Space ([`Overlays`](api/Overlays.md)); their default edge keeps 3:1 contrast over any part, with system colours in forced-colors mode. Its canvas is `aria-hidden="true"`. What it doesn't do, and the page adds: a way to turn the view without a drag, a spoken line for each pick, and the view's name.

## 1. View controls for keyboard and switch users (WCAG 2.1.1, 2.5.7)

2.0 has no keyboard camera: the orbit is a pointer drag and the zoom a wheel or a pinch. Put your own buttons in a sibling of `el` (the runtime owns `el`'s shadow root and renders none of its children) that turn and tilt the view from [`camera.state`](api/Camera.state.md) with [`camera.set`](api/Camera.set.md), and reset it with [`camera.fit`](api/Camera.fit.md) or a named view ([`camera.views.go`](api/Camera.views.md)). A move clamps to the page's [limits](api/Camera.limits.md), as a drag does. The example is spec section 5.6's, compiled against the declaration ([`examples/5.06-camera-2.md`](examples/5.06-camera-2.md)):

```ts
import type { Session } from '@holostep-configurators/runtime-client/contract';
declare const session: Session;
declare const signal: AbortSignal;
declare function addButton(label: string, run: () => void): void;   // your <button> beside the view, never inside `el`
declare function setStatus(text: string): void;                      // your role="status" region's text
declare function describeChoice(group: string, material: string): string; // your catalogue's words
const turn = (byAzimuthDeg: number, byPolarDeg: number) => (): void => {
  const { target, azimuthDeg, polarDeg, distance } = session.camera.state;
  void session.camera.set(
    { target, azimuthDeg: azimuthDeg + byAzimuthDeg, polarDeg: polarDeg + byPolarDeg, distance },
    { durationMs: 300 },
  );
};
addButton('Turn left', turn(-22.5, 0));
addButton('Turn right', turn(22.5, 0));
addButton('View from higher', turn(0, -15));                         // polarDeg 0 looks straight down
addButton('View from lower', turn(0, 15));
addButton('Reset view', () => void session.camera.fit(undefined, { durationMs: 300 }));
// one sentence per pick, once it is shown: never per frame or per write
session.on('material:selected', ({ group, material }) => setStatus(describeChoice(group, material)), { signal });
```

## 2. Status messages (WCAG 4.1.3)

Announce what changed in your own `role="status"` region (an `aria-live="polite"` one), one sentence per pick once it is shown: from the pick's event ([`'material:selected'`](api/Events.material_selected.md) above, or `'committed'` for a change your own write made), never per write, per frame or per camera move. While the first model loads, say "Loading" there and set `aria-busy="true"` on your wrapper; clear both when `idle()` resolves.

## 3. Naming the view (WCAG 1.1.1, 4.1.2)

Name the view on `el` or a wrapper with `role="group"` and `aria-label`, or point `aria-describedby` at your own text summary of the configured product ([`BootOptions.el`](api/BootOptions.el.md)). Never put `role="img"` on `el` or an ancestor while badges are set: an image's children are presentational, so the badges leave the accessibility tree. `label` items are pixels only, so put what they say in the DOM too.

## 4. Never rely on a drag or the wheel alone

Every action a drag does has another way in: a module you drag ([`input.drag`](api/Input.drag.md)) also has buttons or a list that place it, and a view you turn has the controls above. A full-width view on a phone page leaves a vertical swipe and the plain wheel to the page with [`camera.limits({ gestures: 'cooperative' })`](api/CameraLimits.gestures.md), so a shopper scrolling to the options never turns the product by accident. The browser takes the intersection of `touch-action` over the view and its ancestors, so `el` and every ancestor up to the page's scroller must allow `pan-y`: remove `touch-action: none` (Tailwind's `touch-none`) from them, or a vertical swipe still turns the product. The runtime records 'unsupported-spec' at '/camera/limits/gestures', naming the element, when one blocks it.

## 5. Badges

Give each badge a `label` that names the action, the side and the module, never two alike on one module. A `::part(badge)` style of your own keeps a 3:1 edge against the parts behind it, as the default does, and a visible focus outline.
