1 of 70

Accessibility Challenges

with Single Page Applications

Natalie MacLees

COO and Cofounder

2 of 70

What is a SPA?

3 of 70

Single-Page Application (SPA)

…a web app implementation that loads only a single web document, and then updates the body content of that single document via JavaScript APIs…

4 of 70

5 of 70

Why would you build a SPA?

6 of 70

Accessibility Challenges with SPAs

7 of 70

1.

“Page” changes

8 of 70

2.

Focus management

9 of 70

3.

Content changes

10 of 70

4.

Lots of custom components

11 of 70

Managing “Page” changes

Challenge 1

12 of 70

Implement all the usual things

13 of 70

Update history

React Router

Angular Router

Vue Router

14 of 70

Update the document title

15 of 70

Update document.title when component is mounted

Built-in Title service in @angular/platform-browser

16 of 70

<Route path="/about" render={props=> (

<Page {...props} component={About} title="About Us" />

)}

/>

Use render property�of Route component

17 of 70

{

path: "/about",

component: About,

meta: { title: "About Us" }

}

Add meta to routes

18 of 70

router.beforeEach((to, from, next) => {

document.title =

to.meta.title || "Default Title";

next();

});

Update title on beforeEach

19 of 70

Announce the new page title

20 of 70

ARIA live region

<div role="region" aria-label="Page Title" id="pageTitle" aria-live="polite">About Us</div>

21 of 70

ARIA live region

Focus wrapper

<body tabindex="-1">

<div id="app" tabindex="-1">

{/* Application code goes here */}

</div>

</body>>

22 of 70

ARIA live region

Focus wrapper

Focus first focusable element

23 of 70

ARIA live region

Focus wrapper

Focus first focusable element

Focus heading

24 of 70

<body>

<div id="app">

<h1 tabindex="-1">About Us</h1>

</div>

</body>

25 of 70

Focus Management

Challenge 2

26 of 70

Handle modals correctly

27 of 70

Button

28 of 70

Button

ESC

29 of 70

Button

30 of 70

Handle form errors correctly

31 of 70

32 of 70

<label for="email">Email Address</label>

<input type="email" id="email" placeholder="Enter your email" required aria-describedby="email-error">

<p id="email-error">Your email address is required to log in</p>

33 of 70

Content Changes

Challenge 3

34 of 70

Notify users of new content

35 of 70

Confirmation

<div id="list-status" aria-live="assertive">Licorice was deleted</div>

36 of 70

Confirmation

Expected update

37 of 70

Confirmation

Expected update

Information

<div id="report-status" aria-live="polite"></div>

38 of 70

Confirmation

Expected update

Information

Unexpected & critical

39 of 70

Custom Components

Challenge 4

40 of 70

POSH

Plain Old Semantic HTML

41 of 70

HTML tags have meaning

42 of 70

Learn how to correctly use

  • Lists
  • Buttons
  • Links
  • Data tables
  • Input types
  • Headings
  • Figure elements and captions
  • Navigation
  • ARIA landmarks
  • Fieldsets
  • Legends
  • Labels
  • Forms
  • Radios
  • Checkboxes

43 of 70

Use ARIA landmarks

44 of 70

<!-- Banner landmark -->

<header role="banner">

<h1>Welcome to My Simple App</h1>

</header>

<!-- Main landmark -->

<main role="main">

...

</main>

45 of 70

<!-- Navigation landmark -->

<nav role="navigation">

<ul>

...

</ul>

</nav>

46 of 70

<!-- Contentinfo landmark -->

<footer role="contentinfo">

<p>&copy; 2024 Simple App. All rights reserved.</p>

</footer>

47 of 70

Wrap form elements in a <form>

48 of 70

<template>

<form @submit.prevent="submitForm">

<label for="name">Name</label>

<input type="text" id="name" v-model="formData.name">

<button type="submit">Submit</button>

</form>

</template>

49 of 70

Use <fieldsets> and <legends>

50 of 70

<p>Select your preferred hobbies</p>

<input type="checkbox" id="hobby1" name="hobbies" value="reading">

<label for="hobby1">Reading</label>

...

51 of 70

<fieldset>

<legend>Select your preferred hobbies</legend>

<input type="checkbox" id="hobby1" name="hobbies" value="reading">

<label for="hobby1">Reading</label>

...

</fieldset>

52 of 70

Use <label> for all form fields

53 of 70

<form>

<input type="text" placeholder="Full Name" required>

<select required>

<option value="" disabled selected>Select your country</option>

...

</select>

<button type="submit">Submit</button>

</form>

54 of 70

<!-- Text Input -->

<label for="name">Full Name</label>

<input type="text" id="name" name="name" placeholder="Enter your name" required>

<!-- Dropdown/Select Input -->

<label for="country">Country</label>

<select id="country" name="country" required>

<option value="">Select your country</option>

...

</select>

55 of 70

Use <button> and <a> correctly

56 of 70

Update the current page

<a href="/about" aria-current="page">About Us</a>

57 of 70

Avoid interactive <div> and <span>

58 of 70

<div v-on:click="doThis">

<div onClick={doThis}>

59 of 70

The problems with interactive <div>s

  • Not included in focus order
  • Do not respond to enter or space key presses
  • Lack the button role announcement
  • Not supported in high contrast mode
  • Do not have right-click context menus

60 of 70

Can we fix those?

  • Add tabindex=”0”
  • Add keyUp handler in addition to the onClick handler
  • Add ARIA role=”button”
  • …and on and on

61 of 70

…or you could just

62 of 70

<button v-on:click="doThis">

<button onClick={doThis}>

63 of 70

and get your accessibility for FREE

64 of 70

A few last reminders

Freebies

65 of 70

Test everything without a mouse

66 of 70

Ensure focus is visible

67 of 70

Alt text for non-text content

68 of 70

Use skip links

69 of 70

Test, test, and then test some more

70 of 70

Thank you!

Natalie MacLees

natalie.maclees@nsquared.io

@nataliemac