Skip to content

Accessibility

Static Badge Static Badge

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.

<div class="d2m-main" role="application" lang="en">                ← main container
  ├─ <div class="d2m-layers d2m-content-layer"                     ← content layer (the tab stop)
  │       role="img"
  │       aria-roledescription="interactive map"                   (3d: "interactive panoramic view")
  │       aria-label="<accessible name>"
  │       aria-describedby="d2m-desc-{id}"                         (only when description set)
  │       lang="<bcp47>"
  │       tabindex="0">
  │     <!-- drawing surface: <canvas> or <svg> -->
  │
  ├─ <div id="d2m-desc-{id}" class="d2m-sr-only">                  ← hidden description
  │     <!-- supplementary description text, when set -->
  │
  ├─ <div class="d2m-sr-only"                                      ← status announcer
  │       role="status" aria-live="polite" aria-atomic="true">
  │     <!-- driven only by announceStatusMessage() -->
  │
  └─ <div class="d2m-layers d2m-interface-layer">                  ← interface layer
        <!-- viewer UI controls -->

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" plus aria-roledescription lets 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-motion is 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).

const description = viewer.accessibility.getCurrentMapDescription();
// { label: "Chicago Cubs seating chart", description?: "...", language: "en" }

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)

Static Badge

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

1
2
3
4
5
const description = await viewer3d.accessibility.getRemoteViewDescription({
    venue_id: "eu-es-00034-testvenue",
    view_id: "S_B40",
});
// { label: "Section B40, row 4", description?: "...", language: "en" }

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

1
2
3
4
5
viewer.accessibility.setCurrentMapDescription({
    label: "Cubs main level",
    description: "Lower-bowl seating, sections 101 through 145",
    language: "en",
});

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

viewer.accessibility.announceStatusMessage(`Section ${section.id} hovered`);

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 a description passed in the load options is what gets spoken.
  • false: lifecycle announces are suppressed. Manual announceStatusMessage calls 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).

viewer.accessibility.flags.automatic_announce_status_message = false;

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:

1
2
3
4
5
6
7
8
interface DVMDescription {
    label: string; // short accessible name; becomes aria-label
    /** @deprecated alias of `label`, populated with the same value */
    title: string; // retained for backward compatibility; prefer `label`
    description?: string; // longer supplementary text; becomes the
    // textContent of the hidden aria-describedby node
    language: string; // BCP 47 language tag; becomes lang attribute
}
  • label is short. Screen readers read it on focus arrival and after every lifecycle announce. Keep it under one sentence.
  • description is optional and longer. When present, the viewer wires aria-describedby on 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.
  • language is set on both the content layer and the hidden description node. It must be a valid BCP 47 tag (for example en, 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:

1
2
3
4
5
6
7
8
9
viewer.loadMap({
    venue_id: "eu-us-00013-cubs",
    map_id: "main",
    description: {
        label: "Cubs main level",
        description: "Lower-bowl seating, sections 101 through 145",
        language: "en",
    },
});

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:

1
2
3
4
5
viewer.accessibility.setCurrentMapDescription({
    label: "Cubs main level",
    language: "en",
});
viewer.accessibility.announceStatusMessage("Cubs main level");

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.

1
2
3
4
5
6
viewer.subscribe("keydown", (obj) => {
    if (obj.original_event.key === "ArrowRight") {
        const next = nextSection();
        viewer.accessibility.announceStatusMessage(`Section ${next.id}`);
    }
});

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:

1
2
3
4
5
6
viewer.accessibility.flags.automatic_announce_status_message = false;

viewer.loadMap({ venue_id, map_id }).then(() => {
    const count = viewer.getNodesByType("section").length;
    viewer.accessibility.announceStatusMessage(`${venue_id} loaded: ${count} sections available`);
});

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 #4a4a4a for 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:

viewer.subscribe("keydown", (obj) => {
    const event = obj.original_event;
    // 0 = sections view, 1 = seats view
    const current_level = viewer.layers.getLayerLevel();
    switch (event.keyCode) {
        case 37: // ArrowLeft
            current_level === 0 ? prevSection() : prevSeat();
            break;
        case 39: // ArrowRight
            current_level === 0 ? nextSection() : nextSeat();
            break;
        case 13: // Enter
            current_level === 0 ? enterCurrentSection() : selectCurrentSeat();
            break;
        case 27: // Escape
            exitSection(true);
            break;
    }
});

function nextSection() {
    const next = sections[++section_index % sections.length];
    viewer.hover(viewer.getNodeById(next));
    viewer.accessibility.announceStatusMessage(`Section ${next}`);
}

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

Static Badge Static Badge

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 goTo and 3D lookAt) do not read prefers-reduced-motion, so a reduced-motion user still sees full-duration camera animations.
  • 3.1.2 Language of Parts (AA): the viewer's lang attribute is set per load through setCurrentMapDescription / setCurrentViewDescription. There is no built-in way to switch lang for a sub-region of the map or view; integrators that mix languages within one venue must speak the language change through announceStatusMessage and 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 lang for a sub-region of the map or view. Integrators that need this must call setCurrentMapDescription({ language: "..." }) and speak the change through announceStatusMessage.

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.