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. Understandable
  4. Robust

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

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

(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 #{$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

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

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

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

95.9% of home pages had detected WCAG 2 failures. This improved slightly from 96.3% in 2023. Over the last 5 years, the pages with detectable WCAG failures have decreased by only 1.9% from 97.8%. These are only automatically detected errors that align with WCAG conformance failures with a high level of reliability which suggests that the rate of full WCAG 2 A/AA conformance was certainly lower.
WebAim — The WebAIM Million

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

Automate some checks with WAVE web accessibility evaluation tool or Accessibility Insights.

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.