This week we'll look at how to make accessible dynamic content.
As we covered earlier, WAI-ARIA, a.k.a. ARIA, is a specification from the W3C that uses attributes on HTML elements to supplement existing semantics in order to…
This is especially relevant when you are using a front-end JavaScript framework (Angular, React, Vue, et al.).
Remember that the spec tells us that we should use native semantics when they are available.
If it's a button, use a <button>.
The biggest reason:
Support for ARIA features varies wildly.
Some attributes are really well supported. Some are not. Some features work well everywhere, whereas others may be affected by the combination of screen reader and browser.
If you can, use native HTML.
If that's not enough, add aria and test it manually using real screen readers and browsers.
Whatever you can't test manually, base your code on trustworthy sources.
Remember: "No ARIA is better than bad ARIA."
Places that have information on ARIA support:
The role attribute provides information about an element's purpose.
The role attribute identifies the element to the screenreader, but it does not recreate the native functionality. That's on you.
One of the really impressive things about native HTML is the functionality already built-in to elements - especially form inputs. Use semantic elements unless you're sure you know how to recreate them.
| role value | native element (or notes on the role) |
|---|---|
application | When your HTML document behaves completely different from a standard html document; probably don't use this. It turns off all standard keyboard shortcuts in most screenreaders. |
article | <article> |
banner | <header> |
checkbox (or switch if the values are on/off) | <input type="checkbox" /> |
complementary | <aside> |
contentinfo | Information about the document, i.e. copyright. Use <footer> instead. |
definition | <dfn> |
document | Puts the assistive technology into 'reading' mode (which it is by default). |
form | <form> |
heading | <h1> to <h6>; can also extend semantics thusly: <div role="heading" aria-level="7"> |
img | <img src="" alt="" /> |
link | <a href=""> |
listbox | <select> |
listitem (with list) | <li> |
main | <main> |
math | <math
xmlns="http://www.w3.org/1998/Math/MathML"> |
navigation | <nav> |
note | <aside> |
progressbar | <progress max="100" value="70">70%</progress> |
radio (with radiogroup) | <input type="radio" name="a-group" value="choice"> |
scrollbar | CSS: overflow: auto; |
search | <input type="search" /> |
slider | <input type="range" min="0" max="11" /> |
spinbutton | <input type="number" min="10" max="100" /> |
textbox | <textarea></textarea> |
| role value | native element |
|---|---|
application | When your HTML document behaves completely different from a standard html document; probably don't use this. It turns off all standard keyboard shortcuts in most screenreaders. |
article | <article> |
banner | <header> |
checkbox (or switch if the values are on/off) | <input type="checkbox" /> |
complementary | <aside> |
contentinfo | Information about the document, i.e. copyright. Use <footer> instead. |
definition | <dfn> |
document | Puts the assistive technology into 'reading' mode (which it is by default). |
| role value | native element |
|---|---|
form | <form> |
heading | <h1> to <h6>; can also extend semantics thusly: <div role="heading" aria-level="7"> |
img | <img src="" alt="" /> |
link | <a href=""> |
listbox | <select> |
listitem (with list) | <li> |
main | <main> |
math | <math
xmlns="http://www.w3.org/1998/Math/MathML"> |
| role value | native element |
|---|---|
navigation | <nav> |
note | <aside> |
progressbar | <progress max="100" value="70">70%</progress> |
radio (with radiogroup) | <input type="radio" name="a-group" value="choice"> |
scrollbar | CSS: overflow: auto; |
search | <input type="search" /> |
slider | <input type="range" min="0" max="11" /> |
spinbutton | <input type="number" min="10" max="100" /> |
textbox | <textarea></textarea> |
| role value | component |
|---|---|
dialog | A modal, for example |
grid | A navigable table (think of a calendar with selectable dates, for example), with row, rowheader, cell, and columnheader. |
group | A group of UI elements not in a perceivable page section (i.e. not grouped together in the code, but only by the contents' context). |
log | A chat, for example |
marquee | Like a log, but not vital or linear, i.e. a carousel or stock ticker. |
menubar | A menu (not necessarily navigation), with menu, menuitem, and menuitemcheckbox. |
presentation | Similar in function to aria-hidden, this role tells the screenreader that the element is decorative, but support for it on natively focusable elements is kinda sketchy. |
region | A landmark, but where no other section/landmark element (like <main>, <nav>, etc.) or role applies. |
separator | Just a dividing line, like an <hr>, but also makes allowances for an interactive "splitter". |
status | A status update message, i.e. "your changes have been saved". |
tablist | A group of tabs, with the roles tab (for the tab triggers), and tabpanel (for the content revealed by the tab triggers). |
timer | A time keeping element. |
toolbar | Same concept as a menubar, except that… well, from what I understand a menubar has words, and a toolbar has icons, but you'd think that would be a moot point, with the icons' alternative text, so… |
tooltip | An element that provides additional information when focussed/hovered. Note that this is a tricky pattern to do correctly, since it requires the focus-grabbing of a modal, but (often) in the context of focussing on a form element. Avoid tooltips in general when you can! |
tree | With treeitem, describes a navigable tree structure, like folders and files. |
| role value | component |
|---|---|
dialog | A modal, for example |
grid | A navigable table (think of a calendar with selectable dates, for example), with row, rowheader, cell, and columnheader. |
group | A group of UI elements not in a perceivable page section. |
log | A chat, for example |
marquee | Like a log, but not vital or linear, i.e. a carousel or stock ticker. |
menubar | A menu (not necessarily navigation), with menu, menuitem, and menuitemcheckbox. |
| role value | component |
|---|---|
presentation | Similar in function to aria-hidden, this role tells the screenreader that the element is decorative, but support for it on natively focusable elements is interesting Opens in a new window. |
region | A landmark, but where no other section/landmark element (like <main>, <nav>, etc.) or role applies. |
separator | Just a dividing line, like an <hr>, but also makes allowances for an interactive "splitter". |
status | A status update message, i.e. "your changes have been saved". |
tablist | A group of tabs, with the roles tab (for the tab triggers), and tabpanel (for the content revealed by the tab triggers). |
| role value | component |
|---|---|
timer | A time keeping element. |
toolbar | Same concept as a menubar, except that… well, from what I understand a menubar has words, and a toolbar has icons, but you'd think that would be a moot point, with the icons' alternative text, so… |
tooltip | An element that provides additional information when focussed/hovered. Note that this is a tricky pattern to do correctly, since it requires the focus-grabbing of a modal, but (often) in the context of focussing on a form element. Avoid tooltips in general when you can! |
tree | With treeitem, describes a navigable tree structure, like folders and files. |
Aside from role, all other ARIA attributes are prefixed with aria-.
These attributes are broken up into different categories in the documentation, but are all applied to HTML in the same way - either hard-coded or applied via JavaScript.
"Widget" attributes are essentially all concerned with communicating input (or the output based on user input), or letting the user know where they are and what content is available to them.
| Widget attributes |
|---|
aria-autocomplete="none|inline|list|both" (w/ role="combobox|textbox") |
aria-checked="true|false|mixed" (w/ role="checkbox|switch|(etc.)") |
aria-current="page|step|date|location|time|true|false" |
aria-disabled="true|false" |
aria-expanded="true|false" |
aria-haspopup="true|false|menu|listbox|tree|grid|dialog" |
aria-hidden="true|false|undefined" |
aria-invalid="true|false|grammar|spelling" |
aria-label={string} (only for invisible text) |
aria-level={integer} (w/ role="grid|heading|listitem|row|tablist") |
aria-multiline="true|false" (w/ role="textbox") |
aria-multiselectable="true|false" (w/ role="grid|listbox|tablist|tree") |
aria-orientation="horizontal|vertical|undefined" (w/ role="scrollbar|select|separator|slider|tablist|toolbar") |
aria-pressed="true|false|mixed" (w/ role="button") |
aria-readonly="true|false" (w/ role=checkbox|combobox|grid|gridcell| listbox|radiogroup|slider|spinbutton|textbox") |
aria-required="true|false" (w/ role="combobox|gridcell|listbox|radiogroup|spinbutton|textbox|tree") |
aria-selected="true|false|undefined" (w/ role="gridcell|option|row|tab") |
aria-sort="ascending|descending|none|other" (w/ role="columnheader|rowheader") |
aria-valuemax={number} aria-valuemin={number} aria-valuenow={number} (w/ role="range|scrollbar|separator|slider|spinbutton") |
aria-valuetext={string} (w/ role="range|separator") |
| Widget attributes |
|---|
aria-autocomplete="none|inline|list|both" (w/ role="combobox|textbox") |
aria-checked="true|false|mixed" (w/ role="checkbox|switch|(etc.)") |
aria-current="page|step|date|location|time|true|false" |
aria-disabled="true|false" |
aria-expanded="true|false" |
aria-haspopup="true|false|menu|listbox|tree|grid|dialog" |
aria-hidden="true|false|undefined" |
aria-invalid="true|false|grammar|spelling" |
aria-label={string} (only for invisible text) |
| Widget attributes (continued so I can fit this junk onto separate slides) |
|---|
aria-level={integer} (w/ role="grid|heading|listitem|row|tablist") |
aria-multiline="true|false" (w/ role="textbox") |
aria-multiselectable="true|false" (w/ role="grid|listbox|tablist|tree") |
aria-orientation="horizontal|vertical|undefined" (w/ role="scrollbar|select|separator|slider|tablist|toolbar") |
aria-pressed="true|false|mixed" (w/ role="button") |
aria-readonly="true|false" (w/ role=checkbox|combobox|grid|gridcell| listbox|radiogroup|slider|spinbutton|textbox") |
aria-required="true|false" (w/ role="combobox|gridcell|listbox|radiogroup|spinbutton|textbox|tree") |
aria-selected="true|false|undefined" (w/ role="gridcell|option|row|tab") |
| Widget attributes (continued) |
|---|
aria-sort="ascending|descending|none|other" (w/ role="columnheader|rowheader") |
aria-valuemax={number} aria-valuemin={number} aria-valuenow={number} (w/ role="range|scrollbar|separator|slider|spinbutton") |
aria-valuetext={string} (w/ role="range|separator") |
ARIA attributes that are categorized as "live region" attributes are for communicating changes in the document to the user. We often assume that if something "pops up", or disappears from the page, the user will see that change.
Live region attributes all have the purpose of providing an alternative to changes a user would notice visually.
| Live region attributes |
|---|
aria-live="polite|assertive|off" (used with aria-atomic) |
aria-atomic="true|false" |
aria-relevant="additions|text|removals|all" |
aria-busy="true|false" |
Some of these live region attributes are automatically added when we add a particular role to an element - meaning we don't need to add that live region attribute manually.
| Implicit live region attributes |
|---|
| Several roles have implicit live region values: |
role="alert" has an implicit aria-live="assertive" |
role="status" and role="log" have an implicit aria-live="polite" |
role="timer" and role="marquee" have an implicit aria-live="off" |
These can be overridden by explicitly declaring aria-live values. |
This next category of aria attributes defines relationships between elements. Often, relationships between elements (think a label and an input) are shown in the visual layout, but are hard to understand based purely on the DOM (or the audio output of the DOM). This category of attributes works by providing an alternative to the information conveyed by the visual layout.
| Relationship attributes |
|---|
role="application|composite|group|textbox" aria-activedescendant={id} |
aria-controls={id} |
aria-labelledby={id} |
aria-describedby={id} similar to aria-labelledby, but verbose instead of concise |
aria-flowto={id} gives the user the option to move to an element, overriding the source order |
aria-owns={id} Defines a parent-child relationship when one isn't semantically apparent. To understand how this is different from aria-controls, imagine a carousel. The
arrows on the left and right would have aria-controls attributes. The dots at the bottom that correlate to individual slides would have aria-owns attributes. |
role="article|listitem|menuitem|option|radio|tab" aria-setsize={integer} aria-posinset={integer} Only required when not all elements from the set are included in the DOM. |
| Relationship attributes |
|---|
role="application|composite|group|textbox" aria-activedescendant={id} |
aria-controls={id} |
aria-labelledby={id} |
aria-describedby={id} similar to aria-labelledby, but verbose instead of concise |
aria-flowto={id} gives the user the option to move to an element, overriding the source order |
| Relationship attributes (continued) |
|---|
aria-owns={id} Defines a parent-child relationship when one isn't semantically apparent. To understand how this is different from aria-controls, imagine a carousel.
The arrows on the left and right would have aria-controls attributes. The dots at the bottom that correlate to individual slides would have aria-owns attributes. |
role="article|listitem|menuitem|option|radio|tab" aria-setsize={integer} aria-posinset={integer} Only required when not all elements from the set are included in the DOM. |
Let's look at some places where aria attributes might come in handy.
For expandable areas, definitely use the HTML5 details/summary elements Opens in a new window, as they have great native support Opens in a new window in various assistive technologies.
Where that's not possible, you'll have to create your own.
This example is based on the example in the fantastic book Inclusive Components Opens in a new window, by Heydon Pickering, which I highly recommend if you want to get great at creating "curb-cut" style components.
The ARIA dialog role is a powerful one, but there are some things you need to remember:
Let's be polite while our timer is running.
See the Pen abdNeyp by Simon Borer (@simonborer) on CodePen.
Let's use JavaScript to update the aria-busy attribute while our feed is loading.
See the Pen bGEpXYJ by Simon Borer (@simonborer) on CodePen.
Okay, here's an important one: validation.
Here are some of my notes from the code, but it'll probably make more sense when you read the comments in the context of the demo.
Error messages are something that we want to pay particular attention to. Of course, we wouldn't want to tell a user that there is an error message when one hasn't been triggered, so it's never enough just to visually hide error messages.
You'll be expected to know how to create form validation for two reasons:
Note that the 'alert' role is equivalent to aria-live="aggressive", meaning that the user will be interrupted.
A fieldset of radio buttons natively has what's know as a 'roving tabindex'. When tabbing into the group, focus goes to the selected element. You can control the input group with your arrow keys, but tabbing again takes you out of the group. This makes sense, as tabbing through the options when you've already made your selection, or when you're navigating back through the page, wouldn't be ideal.
Radio buttons come with semantic grouping (the fieldset element), but it's a good idea to group together discrete sets of inputs with role="group"
The placeholder attribute is not accessible. That doesn't mean you can't use it - you just need to have an alternative version of the same information.
The pattern attribute is a great way to use client-side validation, but remember - it's not a security measure, it's a Usability feature.
See the Pen qBbaXMr by Simon Borer (@simonborer) on CodePen.
Create a set of accessible 'tabs'.
Requirements:
| Scorecard | |||||||
|---|---|---|---|---|---|---|---|
| Month # | Price | Profit | Total Profit | ||||
| Us | Them | Us | Them | Us | Them | ||
| 1 | $XX | $XX | $XX | $XX | $XXX | $XXX | |
| 2 | $XX | $XX | $XX | $XX | $XXX | $XXX | |
Your group is a business. Each month you set the price on your product. Your expenses are absolutely fixed. Bankruptcy is impossible. Your profits are driven entirely by market competition only with your paired group.
Each month, you must write down 6 figures: what price you chose, what price your competitor chose, what profit you made, what profit they made, what total profit you made, and what total profit they made.
The goal of the game is to maximize your company's profit.
| Your price | Their price | Your profit | Their profit |
|---|---|---|---|
| $30 | $30 | 110 | 110 |
| $30 | $20 | 20 | 180 |
| $30 | $10 | 20 | 150 |
| $20 | $30 | 180 | 20 |
| $20 | $20 | 80 | 80 |
| $20 | $10 | 30 | 150 |
| $10 | $30 | 150 | 20 |
| $10 | $20 | 150 | 30 |
| $10 | $10 | 50 | 50 |
See the Pen Multi-team pepulator by Simon Borer (@simonborer) on CodePen.