HTML is often underestimated – it isn’t complicated and it isn’t strict, and you can start producing results with just a handful of elements. It isn’t creative like CSS, or energetic like JavaScript, but it quietly teams up with the browser to make a lot of the web work – much more than many people realise, and with Web Components it becomes extensible too.
When an HTML document is parsed by a browser, several things happen – the browser creates the Document Object Model (DOM), if an assistive technology is running it creates an Accessibility Tree (ACT), in many cases it renders the element as part of the User Interface (UI), and if the element is interactive it provides the expected interaction support.
If our HTML document contained this HTML:
<button>Play</button>
The browser immediately presents the button in the UI, that button can be focused on using a keyboard, and once functionality has been added using a click event handler, the button can be used by clicking, tapping, pressing the space or enter keys, or by voice command.
If an assistive technology is running, the browser uses the ACT to indicate that the button is in a focusable state, and to indicate the button’s role and accessible name:
- Role: button
- Name: Play
- State: focusable
If you look in the DOM, there’s no sign of the attributes used to provide the keyboard focus and interaction, or any ARIA to provide the accessibility semantics (role, name, and state):
<button>Play</button>
The accessibility properties of native HTML elements are referred to as the element’s default implicit semantics. Hold that thought, we’ll come back to it later. For now the thing to remember is that the whole thing comes as one neat little package – the HTML button element.
Extending the button element
Useful as it is, the button element lacks at least one feature – it doesn’t have any notion of a pressed state. A checkbox can be checked or unchecked, so can a radio button, but a button is just a button no matter how many times you activate it.
When a checkbox or radio button is checked, the browser updates the UI to show its state is now checked, and the ACT is also updated to indicate the same. When a button is pressed the browser does neither of these things, and we have to take on a little of the browser’s responsibility and do it ourselves using CSS and ARIA – CSS to update the visual appearance of the button, and ARIA to update the semantic information in the ACT.
The aria-pressed
attribute can be added to the button element to indicate whether the button is pressed or not (true when the button is pressed, false when it is not pressed):
<button aria-pressed="false">Play</button>
When the browser parses the HTML, it recognises the aria-pressed
attribute and changes the element’s role , and if aria-pressed
is set to true, it also updates the element’s state:
- Role: toggle-button
- Name: Play
- State: focusable, pressed
With the addition of some CSS to alter the appearance of the button both before and after it is pressed, it is possible to simulate the way the browser might do things, if only the native button element had this kind of feature.
Web Components
Enter Web Components, or more specifically Custom Elements. We don’t have the ability to change the button element itself, but with Custom Elements we do have the ability to create a variant of the button element that has this added feature built-in.
There are two types of custom element to choose from that are relevant in this context – customized built-in and autonomous. A customized built-in element takes an existing HTML element and extends its capability, whereas an autonomous custom element is created more or less from scratch.
Customized built-in elements
Since we want to create a variant of the button element, a customized built-in element seems like a good place to start. We can take the HTMLButtonElement and extend it:
class ToggleButton extends HTMLButtonElement { }
customElements.define("toggle-button", ToggleButton, { extends: "button" });
Our custom element is the toggle-button element. All custom elements, whatever their type, must have a dash in their name. It’s a simple way to help the browser and anyone who reads the source code tell the difference between a native element and a custom element.
By extending the HTMLButtonElement
, a custom toggle-button element inherits all the properties of a button element. All we need to do is add the aria-pressed
attribute, and support for mouse interaction (leaving the browser to provide keyboard support on our behalf):
connectedCallback() {
this.setAttribute("aria-pressed", "false");
this.addEventListener("click", togglePressed);
}
function togglePressed() {
const isPressed = this.getAttribute("aria-pressed") === "true";
this.setAttribute("aria-pressed", `${!isPressed}`);
}
A customized built-in element feels like the right approach. It follows established good practice for accessibility by keeping things simple – it inherits all the accessibility properties of the button element, plus the focus and interaction support and a basic UI rendering. We’ve just tweaked it a little to give it the additional ability to be pressed and unpressed.
Unfortunately there are drawbacks to using customized built-in elements. The first is that the code is a bit awkward because we haven’t really created a <toggle-button>
element, we’ve extended a <button>
element.
The HTML to use our customized toggle-button would look like this:
<button is="toggle-button">Play</button>
Ugly code is a relatively minor drawback though. A second drawback to customized built-in elements is that they’re not supported by Webkit – which is a more compelling reason for deciding not to use them.
Autonomous custom elements
An autonomous custom element extends the HTMLElement – the basic building block of all HTML elements:
class ToggleButton extends HTMLElement { }
customElements.define("toggle-button", ToggleButton);
The HTMLElement has no semantic information, no interactive capability, and no default rendering – so we have to assume all the responsibilities of the browser and provide everything (the necessary roles, states, properties and interaction support) , just as though we were reinventing the button element using div or span elements:
connectedCallback() {
this.setAttribute("role", "button");
this.setAttribute("tabindex", "0");
this.setAttribute("aria-pressed", "false");
this.addEventListener("click", togglePressed);
this.addEventListener("keydown", function (event) {
if (event.key === "Enter" || event.key === " ") {
togglePressed();
}
});
}
We use the role, tabindex, and aria-pressed attributes to provide the toggle-button’s role, keyboard focus, and toggle properties. We then need to provide support for both mouse and keyboard interaction.
At first glance an autonomous custom element seems promising. The HTML to use this autonomous toggle-button is just as neat and tidy as the HTML for a native button element:
<toggle-button>Play</toggle-button>
One look at the DOM and it all gets ugly again though. The attributes that provide the role, keyboard focus, and toggle capability of the toggle-button all show up in the DOM (which they wouldn’t do if this was a native button element):
<toggle-button role="button" tabindex="0" aria-pressed="false">Play</toggle-button>
Accessibility Object Model (AOM)
Enter the Accessibility Object Model (AOM).
The AOM is an experimental JavaScript API being incubated at the W3C by Google, Apple, and Mozilla. It proposes several new features intended to solve existing accessibility use cases.
Reflecting ARIA attributes
Most HTML attributes are made up of two different parts – a content attribute and an Interface Definition Language (IDL) attribute (more commonly known as a DOM property).
The content attribute can be set by adding the attribute directly to your HTML, or by using element.setAttribute()
to do the same thing using JavaScript. The DOM property can only be set using element.attribute
in your JavaScript.
When you set the content attribute the DOM property for most attributes automatically reflects the change, and vice versa. In other words, the content and IDL attributes keep themselves in synchrony with each other.
Not all attributes work like this, and ARIA attributes are among the ones that don’t. You can set content attributes like this:
this.setAttribute("role", "button");
this.setAttribute("aria-pressed", "false");
That applies the aria-pressed
attribute to the HTML, just as though we’d written the attribute inline in the HTML itself. The AOM makes it possible to set the DOM property directly, without having to go via the content attribute first:
this.role = "button";
this.ariaPressed = false;
Note that the syntax of the ARIA attribute has changed. Instead of aria-pressed
it is now ariaPressed
. Also note that ariaPressed
takes false as a boolean value, where setAttribute()
takes “false” as a string.
This new capability is defined in the ARIA 1.2 specification, where two interface mixins (ARIARole
and ARIAAttributes
)have been added. Although the AOM is experimental, support for these mixins is already shipping in Safari, and available in Chrome behind a flag (go to chrome://flag and search for “accessibility”, then enable AOM).
You can track support for ARIA attribute reflection by visiting the Web Platform Tests (WPT) harness in different browsers.
Reflecting element references
Attributes like aria-labelledby
and aria-owns
point to the id of another HTML element in the same document, but this creates a problem for Web Components that use the Shadow DOM. The AOM proposes a solution that will mean id references are no longer necessary.
Shadow DOM is another of the specifications that are grouped under the name of Web Components. Like Custom Elements, it gives us access to a feature that was previously only available to the browser.
For example if you use the video element with the controls attribute, the browser provides the video player UI for you. When you look in the DOM, you won’t find any code relating to the buttons, sliders, and other controls of the video element’s rendered UI though. That’s because these controls exist in the shadow DOM not in the DOM – known in this context as the light DOM. Where the browser creates a shadow DOM for certain native elements, the Shadow DOM specification gives us the ability to do the same for custom elements.
The browser keeps the light DOM and the shadow DOM separate, which is why you can’t style native video, audio or some form controls with CSS or target them with JavaScript. Whether it’s a native or a custom element that has a shadow DOM, the line that separates the shadow DOM and the light DOM is known as the shadow boundary – and it can’t be crossed.
The shadow boundary means that a custom element with a shadow DOM is essentially a document in its own right. This affects id attributes in two ways:
- If an id is set inside the shadow DOM, it only has to be unique inside that custom element (not the HTML document the custom element is used in). This means that the custom element can be used multiple times in the same HTML document, without breaking the rule that every id value in a document must be unique.
- If an id is set inside the shadow DOM it can’t be referenced from outside of the custom element – from the light DOM.
The AOM proposes that instead of referencing id attributes, ARIA attributes like aria-describedby
and aria-labelledby
could reference the target elements directly. This means there would need to be a way to pass objects through the shadow boundary and that’s much harder than it sounds, so despite ongoing discussions the AOM has yet to define how it should be done. But once that’s been figured out, the code might look something like this:
this.ariaDescribedByElements = [description1];
This feature has been proposed as a change to HTML, with an open pull request that has the details.
Default semantics for custom elements
We noted earlier that native HTML elements have default implicit semantics – accessibility properties that are neatly hidden away inside the element itself. When we create a custom element we have to add all those properties ourselves, just as we do when we create custom components using div and span elements, and the result is the same in both cases – all the attributes show up in the DOM.
Ideally custom elements should have default implicit semantics just like native elements, and the AOM proposes a way to do this using ElementInternals
. This means that all the accessibility properties would be contained within the custom element, and although authors could still override the default semantics of a custom element (just as we can with native elements), we’d have the ability to set the implicit role and other accessibility properties of custom elements just as the browser sets them for native elements.
An author could create a ElementInternals
object using createInternals()
and use it to set or modify the default implicit semantics of a custom element. For example, we might set the default semantics for the toggle-button custom element like this:
class ToggleButton extends HTMLElement {
var internals = null;
constructor() {
super();
this.internals = customElements.createInternals(this);
this.internals.ariaPressed = "false";
}
}
customElements.define("toggle-button", ToggleButton);
This feature has been proposed as a change to HTML, with an open pull request that has the details.
User action events from assistive technologies
It isn’t possible to listen for events from assistive technologies, and doing so would expose personal data about the person consuming the content, but without this ability it’s difficult to make some interactions accessible – particularly on touch screen devices. The AOM plans to introduce assistive tech events in such a way that ensures user privacy will be protected.
This problem can be demonstrated with a custom slider, either as a web component or created directly in your HTML document. There is an expectation that the position of the slider can be moved (incremented or decremented), and typically you do this on a touch screen device by swiping left or right – unless you have an assistive tech like a screen reader enabled, in which case all the gestures change.
With a screen reader running, the swipe left and right gestures are used to move your focus around – to the next or previous things on the screen, which means they have no impact on the position of a custom slider. If you use a native input element with the range attribute set, the OS recognises that a screen reader is enabled and handles the interaction correctly, but there is no way to emulate that behaviour with a custom slider.
To solve this problem the AOM proposes two things:
- To introduce assistive tech events for things like increment/decrement, scroll up/down, and dismiss.
- To fire assistive tech events and keyboard events simultaneously even when there is no assistive tech running, to hide the fact that the person is using an assistive technology.
For example, if someone pressed the escape key to dismiss a custom dialogue, both the keyboard event and the corresponding assistive tech event would fire – even if there was no assistive tech running at the time.
This part of the AOM is still being discussed and is not yet specified.
Virtual accessibility nodes
There are times when it’s difficult to provide an equivalent fallback to primary content – the HTML canvas element and web XR content being two examples where content is rendered on-screen with no semantic information available. The AOM proposes the idea of adding virtual nodes to the ACT as a way of solving this problem.
The proposed idea borrows from the shadow DOM (so much so that it might as well be called the shadow ACT):
- You would attach an AccessibleRoot to an element in the DOM (that is represented in the ACT).
- An element would only have one AccessibleRoot, just as it can have only one ShadowRoot.
- Once an AccessibleRoot is attached to an element, the DOM children of that element would be ignored in accessibility terms. The light DOM/ACT would not be allowed to mix with the virtual/shadow ACT.
The idea is that it will be possible to create a shadow ACT as fallback content to be used by assistive tech, which will still query the ACT as they do now (using platform accessibility API).
This part of the AOM is still being discussed and is not yet specified.
Full ACT introspection
We have the ability to query the computed styles of an element, and to walk the DOM with JavaScript, but no way to query the computed accessibility of an element or to walk the ACT. The AOM may make both things possible.
The computed style of an element is the actual way it’s presented, after all the different sources of styling information (browser style sheets, authored style sheets, user style sheets etc.) have been applied. The computed accessibility of an element would do the same thing – it would return the actual accessibility properties of an element after all the different sources (browser semantics, authored semantics etc.) have been applied.
Coupled with the ability to walk the ACT (including the shadow ACT) would make it possible to make assertions based on the accessibility properties of an element and respond accordingly – feature sniffing for support of a particular ARIA attribute, or debugging accessibility issues in the console for example.
This part of the AOM is still being discussed and is not yet specified. It’s also likely to be the hardest to achieve because the ACT is not created consistently across browsers. The AOM editors want to make sure that when support for this feature emerges, we won’t have to handle cross-browser compatibility ourselves in our JavaScript.
There are also performance implications that will need to be addressed for this part of the AOM to be viable. For these reasons, the AOM editors have chosen to respect the priority of constituencies and tackle the AOM features that will benefit users first, putting these (more developer and tooling oriented features) last.
First class accessibility
So to return to where we began… Until recently there was an established agreement between developers and browsers – we agreed to use the elements and features of HTML and the browser agreed to create the DOM and the ACT, to render them on screen and to provide the expected interactions.
That agreement has now been completely disrupted – with Web Components we can do it all ourselves. But here’s the thing – when we choose to do it for ourselves, we choose to take responsibility for nearly everything the browser used to do on our behalf.
With Web Components (plus CSS and JavaScript) we have the ability to create new (custom) elements, to create a shadow DOM if one is needed, to visually render the element on screen, and to provide whatever interactions are necessary. What we don’t have is the ability to provide proper accessibility, at least not in quite the same way the browser does it. That effectively makes accessibility a second class citizen of this “brave new world”.
The AOM is a concerted effort by some smart people to change this. If implemented, it will give us the ability to do things like define the default implicit semantics of custom elements, manipulate the ACT, and recognise assistive tech events and trigger the expected actions. In other words it will make accessibility a first class citizen of the extensible web – exactly what it should be.
Muralikrishna Guntupalli says:
Thank you Very much for sharing this knowledge with us. The concept of AOM looks innovative and revolutionary. Looking forward to see this in mainstream.