Playing with the Accessibility Object Model (AOM)

The Accessibility Object Model (AOM) is an experimental JavaScript API that enables developers to modify the browser accessibility tree. The AOM has four phases, and support for phase one recently landed in Chrome Canary behind the flag.

The status quo

Traditionally, access to the accessibility tree has been limited to the platform accessibility APIs used by Assistive Technologies (AT). Even so, the access has been one-way, AT can query the accessibility tree but not manipulate it.

For developers, the only way to manipulate the accessibility tree has been to use ARIA to add, remove, or change the native semantics of HTML elements. Where the semantics (role, name, and state) of native HTML elements are implicit, ARIA forces us to declare the additional semantics explicitly. Using the AOM it’s possible to avoid “sprouting attributes” (as the AOM explainer cheerfully puts it), and instead keep the HTML clean. When it comes to Custom Elements it also means the semantics can be encapsulated within the implementation, instead of added as a “leaky abstraction”.

The AOM

The AOM is being developed by Alice Boxhall and Dominic Mazzoni of Google, James Craig of Apple, and Alexander Surkov of Mozilla. They propose to introduce AOM capabilities in four phases:

  1. Modify the semantic properties of the accessibility node associated with a DOM node;
  2. Directly respond to events or actions from AT;
  3. Create virtual accessibility nodes (not associated with DOM nodes);
  4. Programmatically explore the accessibility tree, and access the computed properties of accessibility nodes.

AOM Phase 1

The AOM Phase 1 specification introduces the AccessibleNode and AccessibleNodeList interfaces. These enable developers to modify the semantics of nodes in the accessibility tree, and to pass references to accessible nodes to other properties.

As an experiment I decided to create a custom disclosure widget using the Accessibility Object Model (AOM) instead of ARIA. For many reasons it’s not advisable to start from a span and a div when you create this kind of interaction, but in the interests of playing with the AOM it serves the purpose.

The underlying HTML looks like this:

<span id="button">Tequila!</span>
<div id="container">
Makes me happy!
</div>

Everything else happens in the JavaScript. First we create references to the DOM nodes that represent the span and div elements:

var button = document.getElementById('button');
var container = document.getElementById('container');

Then modify their properties by setting the tabindex attribute on the span to make it focusable, and the hidden attribute on the div to hide the content:

button.setAttribute('tabindex', 0);
container.setAttribute('hidden', true);

We could then start adding semantics to the DOM node for the span in the usual way:

button.setAttribute('role', 'button');
button.setAttribute('aria-expanded', false);

But instead we can create a reference to the accessible node that corresponds to the DOM node for the span element, and add the semantics straight into the accessibility tree:

button.accessibleNode.role = "button";
button.accessibleNode.expanded = false;

The AOM uses the same set of roles as ARIA. The AOM Phase 1 spec also includes a table that maps AOM properties to their corresponding ARIA attributes (the expanded property corresponds to the aria-expanded attribute for example).

We can then create and call the function that handles the behaviour of the disclosure widget:

    function disclose(event) {

        if(container.getAttribute('hidden')) {
            button.accessibleNode.expanded = true;
            container.removeAttribute('hidden');
        }
        else {
            button.accessibleNode.expanded = false;
            container.setAttribute('hidden', true);
        }
    }

    button.addEventListener('click', disclose, false);
    button.addEventListener('keydown', function(event) {
        if (event.keyCode == 13 || event.keyCode ==32) {
            disclose();
        }
    });

It removes the hidden attribute from the div element, and changes the value of the AOM expanded property on the accessible node for the span element.

The AOM also makes it possible to pass object references to other accessible node properties. We’d usually set the aria-controls attribute on the span element like this:

button.setAttribute('aria-controls', 'container');

But the AOM means we don’t have to pass an idref to the aria-controls attribute to indicate that the span element controls the div element. Instead we create an AccessibleNodeList:

var content = new AccessibleNodeList();

Then we add the accessible node for the div element to the array:

content.add(container.accessibleNode);

Lastly, in the function that handles the disclosure behaviour, we create the association between the accessible nodes for the span and div elements directly in the accessibility tree:

    function disclose(event) {

        if(container.getAttribute('hidden')) {
            button.accessibleNode.expanded = true;
            button.accessibleNode.controls = content;
            container.removeAttribute('hidden');
        }
        else {
            button.accessibleNode.expanded = false;
            button.accessibleNode.controls = null;
            container.setAttribute('hidden', true);
        }
    }

By using an array to store one or more accessible node references, it’s possible to associate multiple accessible nodes with another. Think of the AOM properties that equate to ARIA attributes like aria-labelledby, aria-describedby, or aria-owns, that can take multiple idrefs as values.

Running the experiment

As mentioned at the start, the AOM is an experimental API. For now support for Phase 1 is only available behind the flag in Chrome Canary. This means you need to run Canary from the command line. To do this, open Windows Command Prompt or MacOS Terminal, and navigate to the directory where Chrome Canary is installed.

In Windows run:
chrome.exe --enable-blink-features=AccessibilityObjectModel

In MacOS run:
open -a Google\ Chrome --args --enable-blink-features=AccessibilityObjectModel

You can then open this AOM disclosure demo, and with a screen reader running it’ll behave exactly like you’d expect. The screen reader recognises a button in a collapsed state. When the button is activated, the screen reader recognises the button is now in the expanded state and (Jaws only) recognises that the button is being used to control the disclosed content.

With thanks to Dan Hopkins and Ian Pouncey.

13 comments on “Playing with the Accessibility Object Model (AOM)”

Skip to Post a comment
  1. Comment by Josh OConnor

    Thanks for this Leonie. Very interesting. So if I’m getting this correctly.. It looks like a more object orientated way of adding a11y semantics.

    Where rather than adding ARIA stuff piece by piece, you can define methods that contain these common fixes for UI components, and call them on the objects that you insert into the DOM by the new AccessibleNode and Accessible node list. Is this the idea?

    1. Comment by Léonie Watson

      That’s right, except that nothing changes/affects the DOM. Everything is done in the accessibility tree instead.

      So the way it works at the moment is that we set an attribute (like aria-expanded) on the DOM node that represents the span, and the browser takes that information and exposes it in the accessibility tree. With the AOM, you skip the DOM entirely and set the expanded property directly on the node that represents the span in the accessibility tree.

      1. Comment by Zersiax

        What exactly are the advantages of skipping the DOM and manipulating the AOM directly? Is there a performance benefit?

        1. Comment by Léonie Watson

          I don’t think the impact on performance is known at present.

          The advantage of working directly in the accessibility tree is that it will give us more flexibility than is possible using ARIA alone. It will also enable us to write much cleaner HTML, and in the case of Custom Elements it means that all the accessibility can be encapsulated within the implementation (exactly as implicit accessibility is handled in the browser already).

  2. Comment by Josh OConnor

    Thanks Leonie, and also Zersiax as that was my next question. Is there a user experience benefit to this approach or is it conceived as making a11y annotation easier from the dev side? I guess I’m trying to understand the net benefits.

  3. Comment by Josh OConnor

    Sorry to complete the question.. The net benefits of a11y tree manipulation over direct DOM manipulation. Thanks

    1. Comment by Léonie Watson

      Phase 1 is essentially equivalent to ARIA in capability, but Phases 2, 3, and 4, will introduce far greater capability than we’ll ever have with ARIA alone.

  4. Comment by Josh O Connor

    Sounds intriguing – KUTGW. J

  5. Comment by Alex Rogers

    Interesting! Out of interest, why didn’t you use a button tag?

    1. Comment by Léonie Watson

      I wanted to demonstrate adding both a role and properties to an element, so starting from a semantically neutral span element made sense. As the post notes, this is not how to create a disclosure widget in practice, it’s just for illustrative purposes.

  6. Comment by Craig Francis

    Out of interest, why do you set `.controls = null` when hiding the container?

    As that element is set to hidden, I’d assume the assistive tech would know to ignore it, like when the `aria-controls` attribute is set once and not updated.

    Either way, it’s an interesting approach to working with the AOM… thanks for introducing this 🙂

    1. Comment by Léonie Watson

      At the moment the only screen reader to support aria-controls is Jaws, and it will announce the shortcut for navigating between the control and the controlled element regardless of whether the controlled element is hidden or not.

      The ARIA specification doesn’t specify the expected behaviour. There is an issue open against the ARIA spec, but there is disagreement about the way aria-controls should behave.

  7. Comment by Craig Francis

    Not sure I should comment, but I’d have thought it would be valid to point to a hidden element, but the AT shouldn’t provide broken navigation to it.

    Jaws currently says “use JAWS key plus Alt and M to move to the controlled element”.

    Maybe Jaws should change the message to something like “this controls something that is currently hidden”?

Comment on this post

Will not be published
Optional