Accessibility Object Model

Alexander Surkov, Mozilla
Alice Boxhall, Google
Dominic Mazzoni, Google
James Craig, Apple

Presentation Check

Great! The Accessibility Object Model is enabled in this browser.

Demos should work fine.

Accessibility Object Model support is not enabled.

To enable it in Chrome, use this command-line switch:

        --enable-blink-features=AccessibilityObjectModel
      

Tip: Arrow keys move between slides, but not when something else has focus.

Page Up / Down moves between slides, regardless of focus.

Overview

Background

Assistive technology (AT) augments or replaces the existing UI for an application. Examples include:

Image showing 4 layers of 2-way communication - from the application, to the accessibility tree, to assistive technology, to the user.

Accessibility APIs

Each operating system has its own accessibility APIs for native applications.

Besides assistive technology, these APIs are also often used for automation, for example for password managers or for single-sign-on.

Accessibility Tree

Native applications expose an accessibility tree via accessibility APIs. Each accessibility node has properties such as a role, name, state, and actions.

Visualization of an accessibility tree. The root has role main, the child has role form, then it has three children, a radio button, a slider, and a button, each with role, name, state, and actions pictured.

Accessibility on the web

Web browsers automatically map the DOM into the accessibility tree.

Image showing how the the web browser simultaneously paints the visual UI and also updates the accessibility tree.

Web accessibility APIs

The web has rich support for making applications accessible, but only via a declarative API.

There's always a one-to-one correspondence between a DOM node and a node in the accessibility tree.

Image showing how a div with role=checkbox might simultaneously be drawn as a custom checkbox, while mapping onto a native chcekbox in the accessibility API.

Gaps

There are several key gaps in the web platform and ARIA.

Some gaps are about ergnomonics; it's possible to make an app accessible but the APIs are inconvenient or cumbersome, but usable.

Others gaps make it impossible for web developers to make apps with the same degree of accessibility as native apps.

We're going to focus on five key gaps.

Gap 1: Leaky abstractions

Today, a library author creating a custom element is forced to "sprout" ARIA attributes to express semantics which are implicit for native elements.

    
    

    
    
  

This is mostly just an inconvenience. For the most part it's still possible to make custom elements and other widgets accessible.

Gap 2: Limitations of IDREFs

To express an accessible relationship between two elements in the DOM, ARIA attributes let one element refer to the ID of another element.

aria-labelledby indicates one element labels another:

    
First name:

aria-activedescendant indicates a descendant that's focused in a composite control like a list box.

    
Item 1
Item 2
Item 3

Gap 2: Limitations of IDREFs (2)

It can be cumbersome and error-prone to maintain unique IDs on all elements that have a relationship with some other element.

But if you're using custom elements, it's impossible to establish a relationship between two elements that aren't part of the same tree scope.

For example, it's impossible to make an accessible custom listbox using aria-activedescendant:

    
      Item 1
      Item 2
      Item 3
    
  

Gap 3: No way to capture input events from AT

ARIA only helps the web author communicate in one direction: from the web app to the assistive technology.

However, AT can also command and control applications, for example issuing commands to activate, focus, or scroll. While native HTML elements already support these commands, web authors can't override this behavior for custom elements.

Examples: AT wants to select an item in a list box, increment a slider, or dismiss a dialog.

Gap 4: Every accessible node requires a DOM element.

To make a canvas-based UI accessible requires hacks like fake DOM elements hiding behind the canvas.

Gap 5: No introspection

No way to programmatically detect if an accessibility feature is supported by the browser or if you're using it correctly.

    
    
    
    

Accessibility Object Model

AOM: Brief History

Microsoft and Mozilla independently proposed a JavaScript accessibility API. Apple and Google had similar ideas.

This spec takes the best ideas and organizes them into four phases, each with smaller scope, each solving an increasing number of real-world use-cases.

AOM is being implemented behind a flag in Google Chrome now (currently partway through phase 3).

Phase 1: Accessible Properties

Every DOM element has a corresponding AccessibleNode.

Every ARIA attribute, such as role and aria-checked here:

    
Receive promotional offers

...can be replaced with equivalent JavaScript:

    el.accessibleNode.role = "checkbox";
    el.accessibleNode.checked = true;
  

Phase 1: Accessible Properties (2)

Instead of using an IDREF:

    el.setAttribute('aria-activedescendant', 'child' + childIndex);
  

...just reference another element directly:

    el.accessibleNode.activeDescendant = child.accessibleNode;
  

IDL of Phase 1

    interface AccessibleNode {
      attribute AccessibleNode? activeDescendant;
      attribute boolean? atomic;
      attribute DOMString? autocomplete;
      attribute boolean? busy;
      attribute DOMString? checked;
      attribute long? colCount;
      attribute unsigned long? colIndex;
      attribute unsigned long? colSpan;
      attribute AccessibleNodeList? controls;
      attribute DOMString? current;
      attribute AccessibleNodeList? describedBy;
      attribute AccessibleNode? details;
      attribute boolean? disabled;
      attribute AccessibleNode? errorMessage;
      attribute boolean? expanded;
      attribute AccessibleNodeList? flowTo;
      attribute DOMString? hasPopUp;
      attribute boolean? hidden;
      attribute DOMString? invalid;
      attribute DOMString? keyShortcuts;
      attribute DOMString? label;
      attribute AccessibleNodeList? labeledBy;
      attribute unsigned long? level;
      attribute DOMString? live;
    ...
  
    ...
      attribute boolean? modal;
      attribute boolean? multiline;
      attribute boolean? multiselectable;
      attribute DOMString? orientation;
      attribute AccessibleNodeList? owns;
      attribute DOMString? placeholder;
      attribute unsigned long? posInSet;
      attribute DOMString? pressed;
      attribute boolean? readOnly;
      attribute DOMString? relevant;
      attribute boolean? required;
      attribute DOMString? role;
      attribute DOMString? roleDescription;
      attribute long? rowCount;
      attribute unsigned long? rowIndex;
      attribute unsigned long? rowSpan;
      attribute boolean? selected;
      attribute long? setSize;
      attribute DOMString? sort;
      attribute double? valueMax;
      attribute double? valueMin;
      attribute double? valueNow;
      attribute DOMString? valueText;
    };
  

Phase 1 Demo

    
    
    

Sick Bay

    $("engineering").accessibleNode.role = "link";
    $("sick_bay").accessibleNode.labeledBy = new AccessibleNodeList();
    $("sick_bay").accessibleNode.labeledBy.add(
        $("sick_bay_label").accessibleNode);
    print("Done.");
  

Phase 1: Accessible Properties

Every ARIA attribute, such as role and aria-checked here:

    
Receive promotional offers

...can be replaced with equivalent JavaScript:

    el.accessibleNode.role = "checkbox";
    el.accessibleNode.checked = true;
  

Phase 2: Background on events from AT

As a reminder, AT users are often not generating keyboard, mouse, and touch events.

They issue high-level commands (click, focus, select, dismiss, etc.) directly to their AT, and the AT tells the app to take that action via accessibility APIs.

Phase 2: Accessible Actions / Input Events

Every AccessibleNode is an EventTarget, and you can listen to input events directly from assistive technology.

    button.accessibleNode.addEventListener("accessibleclick", function(event) {
      console.log("Got Click from assistive technology");
    });
  

Supported events include:

    "accessibleclick"
    "accessiblecontextmenu"
    "accessibledecrement"
    "accessibledismiss"
    "accessiblefocus"
    "accessibleincrement"
    "accessiblescrollintoview"
    "accessibleselect"
    "accessiblesetvalue"
  

Phase 2 use cases include...

Phase 2 and privacy

If an AOM event listener is ever called, the web page *definitively knows* that the user is running AT.

Some users may not be comfortable revealing this, due to concerns of discrimination, or of being offered an undesired alternate interface.

So triggering an AOM event listeners brings up a new user permission dialog before the web page can capture the event.

Phase 2 and feature detection

If the user does grant permission, this is great because the app can now enable any extra features needed by AT.

If the user does not grant permission, they can be confident they're getting the same experience as everyone else.

Phase 2 demo: Feature detection

This button has an AOM click listener on it:

Status: I don't know if you're using AT.

Phase 2 demo: Canvas slider

Warp speed:

  $("slider").tabIndex = 0;
  $("slider").accessibleNode.role = "slider";
  $("slider").accessibleNode.valueMin = 1;
  $("slider").accessibleNode.valueMax = 10;
  $("slider").accessibleNode.valueNow = 5;
  $("slider").accessibleNode.addEventListener(
      "accessibleincrement", function() {
    updateSlider(1);
  });
  $("slider").accessibleNode.addEventListener(
      "accessibledecrement", function() {
    updateSlider(-1);
  });

Phase 3: Virtual Nodes

In addition to the AccessibleNode associated with every DOM element, you can construct an AccessibleNode and set its properties from scratch.

We call this a "virtual" AccessibleNode.

    var virtualNode = new AccessibleNode();
    virtualNode.role = "button";
    virtualNode.label = "Play Game";
  

Insert an AccessibleNode as the child of another AccessibleNode, like this:

    document.body.accessibleNode.appendChild(virtualNode);
  

Restrictions on virtual nodes

The arguments to AccessibleNode.appendChild and AccessibleNode.insertBefore must be a virtual AccessibleNode.

In other words, you can't use these methods to rearrange the accessibility tree corresponding to the DOM.

You can only build a "virtual" accessibility tree that "hangs off" of one DOM node.

Bounding boxes

To set the bounding box of a virtual node, provide its left, top, width, and height in pixels, relative to its offsetParent, which can be any ancestor AccessibleNode.

    virtualNode.offsetLeft = 30;
    virtualNode.offsetTop = 20;
    virtualNode.offsetWidth = 400;
    virtualNode.offsetHeight = 300;
    virtualNode.offsetParent = document.body.accessibleNode;
  

Relative coordinates are important for things like a canvas-based UI, because the canvas element might have CSS transforms applied to it.

It's also legal to set offsetLeft, offsetParent, etc. on an AccessibleNode associated with a DOM element, to override its bounding box.

Focus management

To make a node focusable, the focusable attribute can be set. This is similar to setting tabIndex=-1 on a DOM element.

    virtualNode.focusable = true;
  

To focus an accessible node, call its focus() method.

    virtualNode.focus();
  

When a virtual accessible node is focused, input focus in the DOM is unchanged. The focused accessible node is reported to assistive technology and other accessibility API clients, but no DOM events are fired and document.activeElement is unchanged.

Phase 3 Demo

Not yet ready

Phase 4: Introspection

Phase 4 (2)

Phase 4 (3)

Q & A

Broken Demo: List Box

    

Pick from this list:

Item 1
Item 2
Item 3
    var items = document.querySelectorAll("#listbox *");
    var index = 0;
    function update() {
      if (index < 0) index = 0;
      if (index >= items.length) index = items.length - 1;
      $("listbox").accessibleNode.activeDescendant =
          items[index].accessibleNode;
      items[index].classList.add("selected");
    }
    $("listbox").addEventListener('keydown', function(e) {
      items[index].classList.remove("selected");
      if (e.key == "ArrowUp") { index--; }
      if (e.key == "ArrowDown") { index++; }
      update();
      e.preventDefault();
    });
    update();
    print("Done.");