Accessibility¶
The viewer is responsible for its own DOM and ARIA wiring against WCAG 2.2. The surrounding page (headings, landmarks, surrounding controls, contrast of integrator-supplied UI) remains the integrator's responsibility.
What the viewer ships out of the box¶
Every viewer instance creates a DOM tree with ARIA roles and attributes on its
host container. The tree is the same for both viewers; only the CSS prefix and
aria-roledescription differ.
For the 3D viewer swap the d2m- prefix for dvm-v3d- and the
aria-roledescription value for "interactive panoramic view".
How the tree behaves:
role="application"on the main container tells assistive technologies that keystrokes pass through to the viewer (camera + selection). Browse mode would otherwise steal the arrow keys.- The content layer is the single tab stop.
role="img"plusaria-roledescriptionlets screen readers describe the drawing surface as an interactive image of the venue rather than a generic graphic. - The status announcer is the only element the viewer writes to for
polite-region announcements. It is updated exclusively through
viewer.accessibility.announceStatusMessage. - Scene transitions in the 3D viewer are skipped when
prefers-reduced-motionis set. The core camera lookAt and zoom animations still run at full duration (see the reduced-motion entry under Known gaps).
The viewer.accessibility namespace¶
Everything an integrator can override or read about the viewer's accessibility
state lives on viewer.accessibility. Some of them also exist on the viewer
root; those root duplicates are deprecated (see
Migrating from the pre-namespace API).
getCurrentMapDescription() / getCurrentViewDescription()¶
Returns the accessible description of the currently loaded map (or active scene, for 3D). The fields mirror the ARIA attributes currently set on the content layer. Synchronous. Throws if no map / view is loaded.
Targets WCAG 2.2: 4.1.2 Name, Role, Value (A) and 1.3.1 Info and Relationships (A).
It describes the loaded map or active view only, and it reflects any override
you set with setCurrentMapDescription / setCurrentViewDescription. To
describe a 3D view that is not loaded, use
getRemoteViewDescription below.
getRemoteViewDescription(options)¶
3D viewer only. Asynchronous. Resolves the default accessible description of a view, read from the venue resources. Called without arguments it describes the loaded view; called with a view it describes that one, loaded or not, and it does not load anything. Use it when you need to label a view before opening it, on a thumbnail or a menu entry.
It returns the default description, so overrides written with
setCurrentViewDescription are not included. The promise rejects when the view
does not exist, and when no view is passed and none is loaded.
Targets WCAG 2.2: 4.1.2 Name, Role, Value (A) and, when the label goes on a preview image, 1.1.1 Non-text Content (A).
The map viewer has no equivalent: a 2D map's description travels with the map itself, so it is only available once the map is loaded.
setCurrentMapDescription(input) / setCurrentViewDescription(input)¶
Overrides the current map / view's accessible description. Mutates aria-label,
aria-describedby, and lang on the content layer.
This call is silent. It does not push the new label through the live region.
Pair it with announceStatusMessage if the change should be spoken (see
Speak an override).
Use this method for changes that happen after the load. If the description
is known when you call loadMap / loadView3d, pass it in the load options
instead (see Describe the map or view at load time):
that way the automatic load announcement already uses your label instead of
the default one.
Targets WCAG 2.2: 4.1.2 Name, Role, Value (A) and 1.3.1 Info and Relationships (A).
announceStatusMessage(message)¶
Pushes a polite-region update through the status announcer. This is the only
public path that drives the dedicated aria-live region. Use it for transient
status that would otherwise be missed: hover/selection feedback, non-modal error
messages, the result of an integrator-owned action.
Manual announces are unconditional: they are not gated by the auto-announce flag below.
Targets WCAG 2.2: 4.1.3 Status Messages (AA).
flags.automatic_announce_status_message¶
Default: true.
Controls whether the viewer auto-announces lifecycle events through the live
region: every successful loadMap (map-viewer), and every successful
loadView3d / loadSpace3d (3D viewer).
true: the loaded map / view's label is announced after each successful load. The announced label is the resolved one, so adescriptionpassed in the load options is what gets spoken.false: lifecycle announces are suppressed. ManualannounceStatusMessagecalls still fire.
Set this to false when the host application already speaks the load result
through its own UI (avoiding a double-announce) or when the integrator wants
to combine the load message with surrounding state.
Targets WCAG 2.2: 4.1.3 Status Messages (AA).
flags.outline_on_keyboard_focus¶
Default: true.
Toggles the viewer's inset focus ring when focus arrives via the keyboard (Tab navigation or programmatic focus).
Leave this on. The viewer is keyboard-operable; without a visible ring, the focus position is invisible. Only disable it if the host application supplies its own visible focus indicator on the same DOM region.
Targets WCAG 2.2: 2.4.7 Focus Visible (AA). Also related: 2.4.11 Focus Not Obscured (Minimum) (AA) and 2.4.13 Focus Appearance (AAA).
flags.outline_on_mouse_focus¶
Default: false.
Toggles the same inset ring when focus arrives via a pointer (mouse click or touch tap).
Default is off, matching the convention adopted by most native controls:
pointer users see the click result, so the ring is usually redundant visual
noise. Set this to true when the host design system shows a ring regardless
of input modality.
Targets WCAG 2.2: 2.4.7 Focus Visible (AA). Enabling pointer focus indication does not violate any criterion; it is an integrator preference.
The DVMDescription interface¶
Both getCurrent*Description and setCurrent*Description use the same
three-field interface:
labelis short. Screen readers read it on focus arrival and after every lifecycle announce. Keep it under one sentence.descriptionis optional and longer. When present, the viewer wiresaria-describedbyon the content layer to the hidden description node containing this string. Use it for the "what is this and how do I use it" hint.languageis set on both the content layer and the hidden description node. It must be a valid BCP 47 tag (for exampleen,es-MX,de-DE).
Note
title is a deprecated alias of label, kept for backward compatibility. The returned DVMDescription still
includes title (populated with the same value as label), and the input interface (DVMDescriptionInput) still
accepts title as an alias for label. New code should use label.
Common patterns¶
1. Describe the map or view at load time¶
loadMap and loadView3d accept an optional description with the same fields
the setters take. The viewer applies it before the automatic load announcement,
so assistive technologies never hear the default description:
Fields you omit fall back to the default description, the one the venue
material provides or the viewer derives from the map or view id. With the
auto-announce flag on (the default), the one
announcement the load produces already uses your label; with it off, the
load stays silent and the identity is still applied. The description belongs to
its load: the next loadMap / loadView3d resolves its own.
2. Speak an override¶
If the override should be spoken, call the announcer explicitly:
The first call updates the persistent identity. The second pushes the label through the live region. Order matters only for the announce: the identity mutation is independent.
3. Transient navigation status¶
For transient state (arrow-key navigation, hover-driven updates, selection
feedback) use announceStatusMessage. Do not use setCurrentMapDescription
for transient state; that would overwrite the venue's persistent aria-label and
let stale navigation state become the map's accessible name on the next focus
return.
4. Full control over speech¶
To take over every announce (for example, to combine the load result with surrounding state), disable lifecycle auto-announces and drive everything manually:
Manual announceStatusMessage calls are unaffected by the flag.
Focus indicators¶
The viewer paints an inset focus ring on the content layer when it has focus.
The ring is gated independently per input modality through the two flags on
viewer.accessibility.flags:
| Flag | Default | Effect when true |
|---|---|---|
outline_on_keyboard_focus |
true |
Draws ring on Tab / programmatic focus. |
outline_on_mouse_focus |
false |
Draws ring on click / tap focus. |
The ring is a 3px dual-tone gray halo: a 1px light-gray (#f5f5f5) outer
band wraps a 2px dark-gray (#353535) inner band. Both tones meet the 3:1
contrast ratio that
WCAG 2.2: 1.4.11 Non-text Contrast (AA)
requires against a mid-gray backdrop, so the ring stays visible on light and
dark maps and panoramas.
The ring also follows the operating system contrast preference
(prefers-contrast):
no-preference(default): the 3px dual-tone gray ring described above.prefers-contrast: more: swaps to a pure white halo + black inner pair for maximum visibility (OS-level "Increase contrast" setting).prefers-contrast: less: keeps the light-gray halo and softens the inner band to#4a4a4afor users who opted into reduced-contrast rendering.
Keyboard navigation¶
The content layer is the single tab stop on the viewer (tabindex="0"). The
viewer emits keydown events through viewer.subscribe("keydown", handler)
so integrators can implement step / select / zoom on top of the viewer's
internal navigation.
Targets WCAG 2.2: 2.1.1 Keyboard (A).
Pair every navigation step with an announceStatusMessage so assistive-tech
users hear what visual users see:
The keyboard navigation example is a full working walkthrough that covers ordered availability, focus-on-screen behavior, and the seat-level recursion.
Migrating from the pre-namespace API¶
The accessibility surface moved from the viewer root into the
viewer.accessibility namespace. The root methods continue to work but are
deprecated.
| Before (still works, deprecated) | After |
|---|---|
viewer.getMapDescription() (async) |
viewer.accessibility.getCurrentMapDescription() (sync) |
viewer.getViewDescription(obj?) (async) |
viewer.accessibility.getRemoteViewDescription(obj?) (async) |
viewer.setCurrentMapDescription(input) |
viewer.accessibility.setCurrentMapDescription(input) |
viewer.setCurrentViewDescription(input) |
viewer.accessibility.setCurrentViewDescription(input) |
getRemoteViewDescription takes the same input as the root method it replaces and behaves the same way, so
that migration is a rename. getMapDescription has no remote counterpart: it always read the loaded map, which
is what getCurrentMapDescription returns, synchronously.
The namespace splits the two questions the root API answered with one method: getCurrentViewDescription
returns the description currently in effect, including your overrides, and getRemoteViewDescription returns
the default one for a view, loaded or not.
The automatic_announce_status_message flag lives only at
viewer.accessibility.flags.automatic_announce_status_message. The two
focus-outline flags also live only on the namespace.
The DVMDescription interface also changed: the primary field is now label. title
is retained as a deprecated alias of label on the returned
DVMDescription, populated with the same value, so code that reads
result.title keeps working. Prefer result.label; title will be removed in
a future major. On the input side the legacy title is likewise still
accepted as an alias of label. description is now optional.
WCAG 2.2 conformance¶
The criteria below are the ones the viewer surface actively implements (fully, except where a row is marked Partial, see Known gaps). Integrators are still responsible for the rest of the page that hosts the viewer.
| Criterion | Level | Surface |
|---|---|---|
| 1.1.1 Non-text Content | A | getPreview() images have alt and lang set from the view's accessible description; getRemoteViewDescription supplies the same text for previews you render yourself. |
| 1.3.1 Info and Relationships | A | ARIA roles and tree structure on the viewer's containers. |
| 1.4.11 Non-text Contrast | AA | Both focus-ring bands are at least 3:1 against a mid-gray backdrop; the ring follows prefers-contrast. |
| 1.4.13 Content on Hover or Focus | AA | Hover state changes are visual only and reversible; transient nav status flows through announceStatusMessage. |
| 2.1.1 Keyboard | A | Single tab stop on the content layer; integrator-driven nav via subscribe("keydown"). |
| 2.3.3 Animation from Interactions | AAA | Partial. See Known gaps. 3D scene transitions respect prefers-reduced-motion; core camera lookAt / zoom animations do not. |
| 2.4.7 Focus Visible | AA | 3px dual-tone gray ring on the content layer (1px #f5f5f5 outer, 2px #353535 inner, both at least 3:1 against a mid-gray backdrop); adapts to the prefers-contrast setting; controlled by flags.outline_on_keyboard_focus (default on). |
| 4.1.2 Name, Role, Value | A | role="application" on the main container; role="img" + aria-roledescription + aria-label on the content layer. |
| 4.1.3 Status Messages | AA | Dedicated role="status" aria-live="polite" aria-atomic="true" region driven by announceStatusMessage. |
Known gaps:
- 2.3.3 Animation from Interactions (AAA):
respected only by 3D scene transitions. The core camera animations (map
goToand 3DlookAt) do not readprefers-reduced-motion, so a reduced-motion user still sees full-duration camera animations. - 3.1.2 Language of Parts (AA):
the viewer's
langattribute is set per load throughsetCurrentMapDescription/setCurrentViewDescription. There is no built-in way to switchlangfor a sub-region of the map or view; integrators that mix languages within one venue must speak the language change throughannounceStatusMessageand update the description.
Known limitations¶
role="application"suppresses some browse-mode screen-reader features (NVDA's element list, JAWS quick keys). It is necessary because the viewer captures arrow keys for camera and selection, and browse mode would steal them. Users that want structural navigation rely on the integrator's surrounding UI (lists of sections, search, etc.).- Multi-language is per-load: there is no built-in way to switch
langfor a sub-region of the map or view. Integrators that need this must callsetCurrentMapDescription({ language: "..." })and speak the change throughannounceStatusMessage.
Further reading¶
- Map Viewer API: full reference for the map-viewer instance: methods, flags, plugins.
- 3D Viewer API: full reference for the 3d-viewer instance: methods, flags, plugins.
- Keyboard navigation example: a working integration of section / seat keyboard navigation with announcements.