How to not Fail at Building Accessible Web Applications

By

This site is also available as a slideshow.

Get to know your users

Web Content Accessibility Guidelines (WGAG) 2.2 Principles

  1. Perceivable
  2. Operable
  3. Operable
  4. Robust

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.

Example: HTML-only Navbar

<details> elements are a great HTML-only fallback!

Add skip links to improve navigation for keyboard users

Add a few choice ARIA attributes to improve the accessibility of your markup

What are all of the points that we need to consider to make the HTML of a website accessible?

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;
}

(See Blog Post on "Fixing Lists" by Scott O'Hara)

Define CSS properties to tweak the theme based on CSS media features

@media (prefers-reduced-motion: no-preference) {
	--grid-row-transition: grid-template-rows #{$transition-speed} #{$transition-easing};
	--visibility-transition: visibility #{$transition-speed} #{$transition-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) {
	--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

Use a color contrast checker to ensure that all color combinations pass at least the WCAG 2.0 AA level

Focus Styles with sufficient contrast!

With focus-visible we can turn off focus rings for users who are navigating with a mouse.

/* Remove outline styles when the user is not navigating with
the keyboard */
&:focus:not(:focus-visible) {
	border-color: transparent;
	outline: none;
}

Example: Navbar with only HTML and CSS

Add JavaScript to improve the design

Progressively-enhance <details> elements to become a toggle button using aria-expanded

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 ensure that the attribute is correctly defined on the button

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 JavaScript?

If something changes on page, it is necessary to announce this to the user

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>)

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.

Accessibility Evaluation Tooling

96.3% of home pages had detected WCAG 2 failures! This improved very slightly from 96.8% in 2022. Over the last 4 years, the pages with detectable WCAG failures has decreased by only 1.5% from 97.8%. These are only automatically detected errors that align with WCAG conformance failures with a high level of reliability. Because automatic testing cannot detect all possible WCAG failure types, this means that the actual WCAG 2 A/AA conformance level was certainly lower.
WebAim — The WebAIM Million

Use tooling like the WAVE web accessibility evaluation tool or Accessibility Insights to evaluate your application!

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

Next Steps

Subscribe to a newsletter like A11y Weekly to learn more about accessibility and hear stories from different user groups!

Thank you!

The content of this presentation is also available as a microsite.