Building an accessible product sounds so obvious, yet it is very often overlooked or perceived as a separate entity of the product. I, myself, have fallen victim to this frame of thought. Only after having worked on several accessibility-related projects did version 3.4 of pickadate.js come out with some ARIA controls. Not implementing it sooner was mostly due to a lack of time, but partly also due to me expecting it to be a daunting task.
I'd like to stress here how crucial accessibility on the web has become. If your product isn't accessible, it means that it doesn't really fully function. Yes, you might have customers coming to your site - but a large population of visitors will not even be given the chance to be your customer, simply because they cannot access it!
It's like architecting a building without planning for elevators.
It leaves a lost opportunity by not being inclusive. And heck, it's even the law now in several countries. So I think we ought to spend a little time learning the basics of building an accessible application.
All about the semantics
HTML (specifically HTML5) tries to inherently be semantic enough to kick start an accessible application. Screen readers know how to navigate and prioritize content based on your markup flow:
<body> <header></header> <aside></aside> <main> <header></header> <article></article> <footer></footer> </main> <footer></footer> </body>
However, HTML semantics alone cannot appropriately describe all possible content types in an application. And that's when ARIA controls come in.
ARIA (Accessible Rich Internet Applications)
ARIA controls give us the ability to define our own semantics where HTML falls short. These definitions are categorized into four roles:
- Landmark roles define the general regions within an application that become "navigational landmarks" for screen readers - sort of like "Jump to"s.
- Document structure roles add semantics to content organization in a page and are usually not interactive.
- Widget roles describe components of UI widgets.
- Abstract roles just define the ontology of other role types. These are not to be used in an application.
The spec documentation is a bit dry to read through, but essentially, there are three basic steps to building an accessible application:
- Start by architecting the landmark roles,
- Then build out the document structure roles, and
- Finally, create the individual widget roles.
There's a long list of roles that fall under these categories. We will be looking at the most fun part (widget roles) and use a date picker as an example UI component.
There are two types of widget roles:
- Composite roles, and
- Standalone roles.
A composite role defines a "container" for the widget's UI components whereas the standalone roles are components within the UI's container.
So, let's say the basic interaction for the date picker we want is that the user can choose a date from a grid of choices. The following is our markup to start with (for brevity, I'm ignoring irrelevant HTML attributes):
<input> <table> ... <tr> ... <td>19</td> <td>20</td> ... </tr> ... </table>
A screen reader visiting this markup sees two disparate pieces of DOM elements. One an
input that semantically accepts a value, and the other a
table that semantically displays tabular data.
Already, we have a problem with those semantics. We aren't using this
table for tabular data. We're using it to visually present the content in a grid layout - which is just easily achieved with the native
In order to correct this, we need to redefine the role of our
It is crucial to note here that most semantic HTML elements cannot have their roles redefined. Only certain elements can be redefined certain other roles. For the most part,
spans can be redefined to mean anything.
Anyways, from the long list of roles, the most appropriate role for our scenario is the composite
grid role. The
grid role requires that there be at least one element with the role of
gridcell contained by an element with the role of
row, which in turn is owned by the
Essentially that means the following roles need to be in our markup:
<input> <table role="grid"> <tr role="row"> ... <td role="gridcell">19</td> <td role="gridcell">20</td> ... </tr> </table>
When a screen reader comes across the markup above, the following accessibility tree is generated:
<input></input> <grid> <row> ... <gridcell></gridcell> <gridcell></gridcell> ... </row> </grid>
... redefining the semantic meaning of the
table container and content within.
There's many more roles that could fit this scenario - it all depends on the interaction behavior we want the widget to have. An important role to be aware of is
presentation. This role removes all semantic meaning from an element:
<h1 role="presentation">I will be semantic-less</h1> <span>I am inherently without any semantics</span> <ul role="presentation"> <li>I have no semantics either</li> <li>But I have <b>some</b></li> </ul>
When the screen reader sees this markup, the accessibility tree looks like this:
<>I will be semantic-less</> <>I am inherently without any semantics</> <> <>I have no semantics either</> <>But I have <b>some</b></> </>
spans are by definition already semantic-less. And any semantic HTML element that expects certain children elements will strip their semantics as well.
Globally inherited states and properties
Every element inherits some global states and properties that describe it's... well, states and properties.
Looking back at our accessibility tree, we still need to address the hierarchal issue. The elements sit as two disparate DOM elements with no connection to each other. However, we want to describe a parent/child relationship between the
input and the
table to appropriately represent the composite widget.
So, we use the
aria-owns property by passing a space separated list of element IDs that it "contains" but cannot already be semantically inferred from the DOM:
<input aria-owns="datepicker_table"> <table role="grid" id="datepicker_table"> <tr role="row"> ... <td role="gridcell">19</td> <td role="gridcell">20</td> ... </tr> </table>
When the screen reader now comes across this markup, it renders the following accessibility tree:
<input> <grid> <row> ... <gridcell></gridcell> <gridcell></gridcell> ... </row> </grid> </input>
That's more like it. Now the
grid is "contained" by the
input as we intended it to.
Role inherited states and properties
So far we've only talked about attributes that allow us to define semantics and relationships between types of elements. But UI components also inherit further states and properties based on their
role definition. These states must be updated as the user interacts with the widget.
To fix our date picker example, we need to make two additions that will describe:
- That the
gridcells "control" the value of the `input element, and
gridcellis currently selected.
<input id="datepicker" aria-owns="datepicker_table"> <table role="grid" id="datepicker_table"> <tr role="row"> ... <td role="gridcell" aria-controls="datepicker">19</td> <td role="gridcell" aria-controls="datepicker" aria-selected="true">20</td> ... </tr> </table>
This tells the screen reader that the UI component the
gridcell controls has this option as "selected".
We're now starting to scratch the surface of what's capable in building an interactive accessible widget. There's a whole list of more widget and global states and properties that we can use to make this a fuller and more complex widget.
<input id="datepicker" aria-owns="datepicker_table" aria-haspopup="true"> <table id="datepicker_table" role="grid" aria-hidden="true"> <tr role="row"> ... <td role="gridcell" aria-controls="datepicker">19</td> <td role="gridcell" aria-controls="datepicker" aria-selected="true">20</td> ... </tr> </table>
And then switch the
false when the picker opens.
Learn by doing
This is a very basic example with a lot of room to add functionality and improve based on the interaction desired. I would suggest going through the list of all role definitions and the list of all states and properties that can be used to define semantics.
As authors of tools and technologies that are built to help the community, it is our responsibility to make sure we produce solutions that are accessible to everyone.
The current state of the web is scarily inaccessible because of the false notion that implementing accessibility features is complicated and difficult. Well yeah, it does requires some extra effort, but the benefits reaped far outweigh the effort needed. So let's do our part in building a more inclusive community on the web.