aom

ARIA Reflection Explainer

Authors:

Introduction

Forked from the Accessibility Object Model explainer.

This document describes the proposed web standard for reflecting ARIA attributes, meaning that web authors can get and set ARIA attributes on DOM elements directly via JavaScript APIs, rather than by using setAttribute and getAttribute.

As a simple example, instead of writing this code:

el.setAttribute("role", "button");
el.setAttribute("aria-pressed", "true");

you could write this more natural code instead:

el.role = "button";
el.ariaPressed = "true";

Even more important, instead of needing to use an IDREF in order to express a relationship between two elements:

el.setAttribute("aria-labelledby", "label-id");

you could instead directly reference an element.

el.ariaLabelledByElement = labelEl;

Motivating use cases

The motivating use cases all revolve around the challenge in setting relationship properties without needing to use IDREFs

Reflecting simple ARIA attributes

For the role attribute and all other ARIA attributes where the type is a boolean, enum, number, or string, we will reflect these ARIA attributes on HTML elements, e.g.:

el.role = "button";
el.ariaPressed = "true";
el.ariaDisabled = "false";

The reflection is bidirectional:

el.setAttribute("aria-atomic", "true");
assert el.ariaAtomic == "true";

Spec/implementation status

This is now a part of the ARIA 1.2 spec.

This is shipping in Safari, Chrome, and Edge and it’s implemented behind a flag (accessibility.ARIAReflection.enabled) in Firefox.

Reflecting Element references

Straight reflection of ARIA properties would reflect relationship attributes like aria-labelledby as strings:

el.ariaDescribedBy = "id1";

results in

<div aria-describedby="id1"></div>

We propose augmenting this API with non-reflected properties which take element references:

el.ariaDescribedByElements = [labelElement1, labelElement2];
el.ariaActiveDescendantElement = ownedElement1;

Note: the Element or Element suffixes are a naming choice for the reflected property, and do not imply that there will be both string and Element properties for the same attribute.

This would allow specifying semantic relationships between elements without the need to assign globally unique ID attributes to each element which participates in a relationship.

Moreover, this would enable authors using open ShadowRoots to specify relationships which cross over Shadow DOM boundaries.

Querying relationship attributes

When an ARIA relationship attribute is set in the HTML DOM, it can be observed via new reflected attributes. This example demonstrates it with aria-activedescendant, which is reflected as ariaActiveDescendantElement.

<div id='fruitbowl' role='listbox' aria-activedescendant='apple'>
  <div id='apple'>I am an apple</div>
</div>
// We can observe the active descendant relationship through the content attribute.
assert_equals(fruitbowl.getAttribute("aria-activedescendant"), "apple");

// And we can also observe it through the new api.
assert_equals(fruitbowl.ariaActiveDescendantElement, apple);

When an ARIA attribute takes a list of IDREFs rather than a single one, the reflected attribute behaves like an array of elements.

<span id="l1">Street name</span>
<input aria-labelledby="l1 l2">
<span id="l2">(just the name, no "Street" or "Road" or "Place")</span>
// We can observe the labelled-by relationship through the content attribute.
assert_equals(input.getAttribute("aria-labelledby"), "l1 l2");

// And we can also observe it through the new api.
assert_equals(input.ariaLabelledByElements.length, 2);
assert_equals(input.ariaLabelledByElements[0], l1);
assert_equals(input.ariaLabelledByElements[1], l2);

Sprouting relationship attributes

When using reflection to set ARIA relationship properties, the element may “sprout” new attributes.

<div id='fruitbowl' role='listbox'>
  <div id='apple'>I am an apple</div>
</div>
// We make the active descendant of the fruitbowl the apple through our new api.
fruitbowl.ariaActiveDescendantElement = apple;

// We can observe the relationship.
assert_equals(fruitbowl.ariaActiveDescendantElement, apple);
// The fruitbowl has even sprouted a matching content attribute.
assert_equals(fruitbowl.getAttribute("aria-activedescendant"), "apple");

Attributes sprout only at the time the relationship is set, and only if certain conditions are met.

If the target element does not have an ID attribute, no content attribute will sprout, even though the relationship will still be communicated correctly to assistive technology.

<div id='fruitbowl' role='listbox'>
</div>
const apple = document.createElement("div");
apple.innerHTML = "I am an apple";
fruitbowl.appendChild(apple);

// We make the active descendant of the fruitbowl the apple.
fruitbowl.ariaActiveDescendantElement = apple;
// We can observe this relationship as the apple is not in a valid scope.
assert_equals(fruitbowl.ariaActiveDescendantElement, apple);

// No content attribute is sprouted as apple lacks an id.
// However, the relationship is still there, it's still communicated
// to assistive technology. It's just invisible from the DOM.
assert_equals(fruitbowl.getAttribute("aria-activedescendant"), "");

In this example, attempting to set a relationship to an element that’s not attached to the document yet will not sprout an attribute - and subsequently if the target element is added to the DOM, it still won’t have a content attribute because content attributes are only sprouted at the time the property is set.

<div id='fruitbowl' role='listbox'>
</div>
const apple = document.createElement("div");
apple.setAttribute("id", "apple");
apple.innerHTML = "I am an apple";

// We make the active descendant of the fruitbowl the apple.
fruitbowl.ariaActiveDescendantElement = apple;

// NB: we are appending *after* setting.
fruitbowl.appendChild(apple);

// We can observe this relationship as the apple is not in a valid scope.
assert_equals(fruitbowl.ariaActiveDescendantElement, apple);

// No content attribute is sprouted as apple was in an invalid scope when set.
assert_equals(fruitbowl.getAttribute("aria-activedescendant"), "");

Relationships are validated on get

Relationships are validated on get, and are kept intact through invalid states.

<div id='fruitbowl' role='listbox'>
  <div id='apple'>I am an apple</div>
</div>
<div id='shadowFridge'></div>
const apple = document.getElementById("apple");
const shadowRoot = shadowFridge.attachShadow({mode: "open"});

// We make the active descendant of the fruitbowl the apple.
fruitbowl.ariaActiveDescendantElement = apple;
assert_equals(fruitbowl.ariaActiveDescendantElement, apple);
assert_equals(fruitbowl.getAttribute("aria-activedescendant"), "apple");

// We then move the referenced element (apple) into shadow DOM (fridge).
shadowRoot.appendChild(apple);
// The active descendant relationship is now non-observable.
assert_equals(fruitbowl.ariaActiveDescendantElement, null, "computed attr-assoc element should be null as referenced element is in an invalid scope");
// NB: The attribute still exposes the referenced element's id.
assert_equals(fruitbowl.getAttribute("aria-activedescendant"), "apple");

// Move the referenced element (apple) back out of the shadow DOM (fridge).
fruitbowl.appendChild(apple);
// Our active descendant relationship remained intact!
assert_equals(fruitbowl.ariaActiveDescendantElement, apple);

Multi-valued (array) relationship attributes are supported

ARIA relationship attributes that take a list of IDREFs reflect to an array of IDREFs.

<div id='fruitbowl' role='listbox'>
  <div id='apple'>I am an apple</div>
  <div id='pear'>I am a pear</div>
</div>
const apple = document.getElementById("apple");

fruitbowl.ariaDescribedByElements = [apple, pear];
assert_array_equals(fruitbowl.ariaDescribedByElements, [apple, pear]);
assert_equals(fruitbowl.getAttribute("aria-describedby"), "apple pear");

// Temporarily remove apple from the document.
apple.remove();
// The relationship with apple is hidden but kept intact.
assert_array_equals(fruitbowl.ariaDescribedByElements, [pear]);

// Reinsert apple back into the document.
fruitbowl.appendChild(apple);
// The relationship with apple remained intact!
assert_array_equals(fruitbowl.ariaDescribedByElements, [apple, pear]);

Multi-valued (array) relationship attributes may violate equality expectations

When interacting with a reflected multi-valued (array) ARIA relationship attribute, it may violate typical JavaScript object property expectations. For example, if you retrieve the same array twice in a row, the two returned objects may not be the same object using a === equality check.

<div id='fruitbowl' role='listbox'>
  <div id='apple'>I am an apple</div>
  <div id='pear'>I am a pear</div>
</div>
const apple = document.getElementById("apple");

let array1 = [apple, pear];
fruitbowl.ariaDescribedByElements = array1;

// Retrieving the same property won't equal the array that we set it to:
assert_false(array1 === fruitbowl.ariaDescribedByElements);

// However, the contents will be the same:
assert_true(array1.length == fruitbowl.ariaDescribedByElements.length);
assert_true(array1[0] === fruitbowl.ariaDescribedByElements[0]);
...

// Even accessing the same property twice won't result in the same object:
let array2 = fruitbowl.ariaDescribedByElements;
let array3 = fruitbowl.ariaDescribedByElements;
assert(false(array2 === array3);

Spec/implementation status