How to not Fail at Building Accessible Web Applications
By Joy HeronThis site is also available as a slideshow.
Get to know your users
- 👩🏻🦯Users with visual impairments
- 😩Users with cognitive limitations
- 🧏🏿Users with auditory impairments
- 🎨Users with different color perceptions
- 🏎️Users with epilepsy or motion sickness
- ⌨️Users with physical impairments
- 📱Users who cannot afford expensive hardware
Web Content Accessibility Guidelines (WGAG) 2.2 Principles
Step 1: Begin by building and testing the HTML layer
Consider multiple possible semantic HTML layers which would work as a base
Document your thoughts as you go!
It may be necessary to add minimal amounts of JavaScript to test any widgets that we want to add.
Add a few choice ARIA attributes to improve the accessibility of your markup
- Add a descriptive
aria-label
to eachul
unordered list - Indicate link to current page with
aria-current="page"
- Add
role="list"
to all lists where we are going to change thedisplay
property with CSS (See Blog Post on "Fixing Lists" by Scott O'Hara)
Example: HTML-only Navbar
<details>
elements are a great HTML-only fallback!
Add skip links to improve navigation for keyboard users
What are all of the points that we need to consider to make the HTML of a website accessible?
Step 2: Add a CSS layer to implement the design
Style skip links so that they are hidden except when focused
.skip-link {
/* ...styles for skip link... */
&:not(:focus) {
@extend %sr-only;
}
}
Connect CSS rules to ARIA attributes to ensure HTML is correct
/* Use CSS rules to enforce the accessibility of the current link
within the navbar. If it is visually correct, it will also be
semantically correct. */
a[aria-current='page'] {
text-decoration: underline;
text-decoration-color: $active-color;
text-decoration-thickness: 3px;
text-underline-offset: $spacer-xs;
}
See Blog Post on "Using CSS to Enforce Accessibility" by Adrian Roselli
Add rule only removing list styles when we have added a role
attribute to a list
/* Removing `list-style` attributes causes the list to no longer
appear as a list in the accessibility tree. As a workaround, this
only removes the list styling once the ARIA `role="list"` has
been set to reinstate the list semantics for the list. */
ul[role='list'],
ol[role='list'] {
list-style: none;
list-style-type: none;
}
Define CSS properties to tweak the theme based on CSS media features
@media (prefers-reduced-motion: no-preference) { /* ❤️ 🏎️ */
--grid-row-transition: grid-template-rows #{$speed} #{$easing};
--visibility-transition: visibility #{$speed} #{$easing};
}
@media (forced-colors: active) { /* ❤️ 🎨 */
--forced-colors-link-border-width: 0;
--forced-colors-action-margin: #{$navbar-focus-outline-width};
--forced-colors-link-decoration: underline;
--forced-colors-navbar-toggler-width: 10rem;
}
@media (pointer: fine) { /* ❤️ 📱 vs. 🖱️ */
--pointer-fine-navbar-meta-font-size: #{$small-font-size};
--pointer-fine-navbar-meta-spacer: #{$spacer-xs * 0.5};
}
Use defined CSS properties instead of hard coded values
&[data-expanded] {
grid-template-rows: var(--main-navigation-grid-template-rows);
transition: var(--grid-row-transition);
.navigation-links,
.navigation-meta {
overflow: hidden;
min-height: 0;
transition: var(--visibility-transition);
visibility: var(--main-navigation-menu-visibility);
}
}
Note that it is finally possible to animate height: auto;
by animating the grid-template-rows
property!
(See Blog Post on "Animating height: auto" by Nelson Menezes)
Important Media Queries
-
prefers-reduced-motion
— ALWAYS reduce motion when a user requests it to prevent possible epileptic event or motion sickness 🏎️ -
forced-colors
,prefers-contrast
, andprefers-color-scheme
indicate different user color needs 🎨 -
pointer
andhover
can indicate usage of mobile devices 📱
Use a color contrast checker to ensure that all color combinations pass at least the WCAG 2.0 AA level
Ensure focus Styles have sufficient contrast!
With focus-visible
we can turn off focus rings for users who are navigating with a mouse.
/* double ring for focus to improve contrast */
&:focus {
border-color: $navbar-link-hover-color;
outline: 2px solid $navbar-lightened-bg;
}
/* Remove outline styles when keyboard not being used */
&:focus:not(:focus-visible) {
border-color: transparent;
outline: none;
}
Example: Navbar with only HTML and CSS
Step 3: Add JavaScript to improve the design
Progressively-enhance <details>
elements to become a toggle button using aria-expanded
needed here "only" to add animation to submenu toggle and hover
<sub-menu>
<details>
<summary>
...
</summary>
<ul role="list"
aria-label="...">
...
</ul>
</details>
</sub-menu>
➡️
<sub-menu>
<button type="button"
aria-expanded="false">
...
</button>
<div>
<ul role="list"
aria-label="...">
...
</ul>
</div>
</sub-menu>
sub-menu
custom element
Code for sub-menu
custom element
export default class Submenu extends HTMLElement {
connectedCallback() {
let summary = this.querySelector('summary')
let ul = this.querySelector('ul')
if (!summary && !ul) {
return
}
// With our method of animating the collapsing/expanding of
// the menu, it is necessary to wrap the content of the menu
// in a `<div>`
this.innerHTML = `<button type="button" aria-expanded="false">${summary.innerHTML}</button><div>${ul.outerHTML}</div>`
this.button.addEventListener('click', () => this.toggle())
this.navigation.addEventListener('submenu-toggle', (ev) => {
if (ev.detail && ev.detail.expanded &&
ev.target !== this.button) {
if (this.button.getAttribute('aria-expanded') === 'true') {
this.toggle(false)
}
}
})
this.toggle(false)
}
toggle(expanded = !(this.button.getAttribute('aria-expanded') === 'true')) {
let button = this.button
button.setAttribute('aria-expanded', expanded)
button.dispatchEvent(new CustomEvent('submenu-toggle', { detail: { expanded }, bubbles: true }))
}
get navigation() {
return this.closest('ul')
}
get button() {
return this.querySelector('button')
}
get div() {
return this.querySelector('div')
}
get ul() {
return this.querySelector('ul')
}
}
customElements.define('sub-menu', Submenu);
Hook CSS styles for showing/hiding the menu to the aria-expanded
attribute
This ensures that the attribute is correctly defined on the button
sub-menu {
button[aria-expanded='false'] + div {
/* styles for collapsed submenu */
}
button[aria-expanded='true'] + div {
/* styles for expanded submenu */
}
}
Full CSS Code for collapsing and expanding submenu
Full CSS Code for collapsing and expanding submenu
sub-menu {
div {
display: grid;
overflow: hidden;
grid-template-rows: var(--submenu-grid-template-rows);
transition: var(--grid-row-transition);
}
ul {
min-height: 0;
margin-bottom: $spacer-xs;
transition: var(--visibility-transition);
visibility: var(--submenu-visibility);
}
button[aria-expanded='false'] + div {
--submenu-grid-template-rows: 0fr;
--submenu-visibility: hidden;
}
button[aria-expanded='true'] + div {
--submenu-grid-template-rows: 1fr;
--submenu-visibility: visible;
}
}
sub-menu {
div {
display: grid;
overflow: hidden;
grid-template-rows: var(--submenu-grid-template-rows);
transition: var(--grid-row-transition);
}
ul {
min-height: 0;
margin-bottom: $spacer-xs;
transition: var(--visibility-transition);
visibility: var(--submenu-visibility);
}
button[aria-expanded='false'] + div {
--submenu-grid-template-rows: 0fr;
--submenu-visibility: hidden;
}
button[aria-expanded='true'] + div {
--submenu-grid-template-rows: 1fr;
--submenu-visibility: visible;
}
}
Example: Navbar with HTML, CSS and JavaScript
When else may I need want JavaScript?
If something changes on page, it is necessary to announce this to the user
-
Create an HTML element with
role="status"
andaria-live="polite"
and update it to announce changes for assistive technologies. - it's usually necessary for the element to already exist before its content is changed in order for its content to be announced
- 🔥 Tip: integrate area for announcements into the visual design to help all users, not only users of assistive technology
First rule of ARIA
If you can use a native HTML element or attribute with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so.
Know your widgets
Widget | Pay attention to... |
---|---|
Disclosure Widget |
aria-expanded is an attribute on the button, the expanded content should directly
follow the button that expands it
|
Tabbed Interfaces | It shouldn't look like tabs unless it behaves like tabs. Tab selection should occur by arrow keys not the TAB-key. (Most tab components don't do this correctly) |
Dialogs |
Need to implement focus trapping and make sure ESC-key shuts the dialog
(usually the user also wants to close it by clicking outside of the dialog).
It's also critical that there is a focussable close button within the dialog.
Use <dialog> element.
|
Listboxes |
Options in list should be able to be selected with arrow keys
(prefer using a built-in <select> )
|
ARIA live regions |
Change content of an aria-live="polite" region to announce changes to user.
|
Accessibility Evaluation Tooling
How to test your web application
For a user with... | ...test with... |
---|---|
visual impairments 👩🏻🦯 | screenreaders |
cognitive limitations 😩 | 200% zoom, user tests |
auditory impairments 🧏🏿 |
sound turned off (ensure transcripts, closed captions available) |
different color perceptions 🎨 | color contrast checkers, color blindness simulators |
epilepsy or motion sickness 🏎️ |
prefers-reduced-motion: reduce activated!
|
physical impairments ⌨️ | keyboards |
inexpensive hardware 📱 | cheap mobile devices |