0% found this document useful (0 votes)
236 views137 pages

The Bootcampers Guide To Web Accessibility

This document provides an overview of an accessibility book for web developers. It includes a preface with notes from the author about the intended audience and goals of the book. The book is divided into four parts that cover the basics of accessibility, ARIA, JavaScript interactions, and testing. It aims to help readers feel confident in understanding accessibility, common issues, ARIA, interactive patterns, and testing abilities.

Uploaded by

Nguyen Duy Hai
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
236 views137 pages

The Bootcampers Guide To Web Accessibility

This document provides an overview of an accessibility book for web developers. It includes a preface with notes from the author about the intended audience and goals of the book. The book is divided into four parts that cover the basics of accessibility, ARIA, JavaScript interactions, and testing. It aims to help readers feel confident in understanding accessibility, common issues, ARIA, interactive patterns, and testing abilities.

Uploaded by

Nguyen Duy Hai
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 137

1

Preface 4
Authors Notes 5
Who is this book for 6
Why I am writing this book 7
Goals 8
Thank you 8

Part 1 - The Basics 9


Chapter 1 - What is Web Accessibility 10
Chapter 2 - Common HTML Accessibility Errors (and how to Fix them) 18
2.1 Images and Alternative Text 18
2.2 Forms 22
2.3 Color 29
2.4 Buttons & Links 31
2.5 Heading Structure 32
2.6 Overview of Semantic HTML 33
2.7 Focus States & Tabindex 36
2.8 Skip Links 38
2.9 Tables 38
2.10 Languages 40

Part 2 - ARIA 41
Chapter 3 - What is ARIA? 42
Chapter 4 - The Accessibility Tree & AOM 44
Chapter 5 - When to Use ARIA & Most Useful ARIA Attributes 49

Part 3 - JavaScript and Interaction 57


Chapter 6 - Patterns 58
6.1 Modals & Dialogs 58
6.2 Accordions 70
6.3 Tabs 79
6.4 Live Regions 84
6.5 Combobox 87
6.6 Hamburger Navigations 88
6.7 Client Side Routing 93
6.8 React Libraries and Resources 94
Chapter 7 - Progressive Enhancement 95

2
~ llh\.!,/lindsey
wit1Jl
Chapter 8 - User Preferences 100

Part 4 - Testing 109


Chapter 9 - Automated Testing 111
Chapter 10 - Manual Testing & User Testing 132

Conclusion 136

3
~ llh\.!,/lindsey
wit1Jl

Preface 

   


~ llh\.!,/lindsey
1Jl
wit

Authors Notes 
First, in this book, there will be some mentions of ableism. If anything has the potential to be 
triggering, I will start with “Trigger
​ Warning: Ableism.” ​ After the statement is complete, I will say, 
“End
​ Trigger Warning.” ​  
 
Second, as a cognitively disabled person, I prefer identity-first language. Many disabled people 
prefer identity-first language as well. However, I wanted to include this note as some people prefer 
person-first language (i.e., “people with disabilities”). If you prefer that, I respect that. But I did 
want to clarify that I do not use that language often in this book. 
 
Third, I am a mac user. Most of the screen reader examples will reflect the native screen reader, 
VoiceOver. VoiceOver works best when you use it with Safari, as both are native to macOS. 
 
Fourth, I write most of the time, when I write my sources or additionally resources, I’ll post them 
toward the end of the section that I used that source for. Occassionally, I use footnotes for direct 
quotes or extra context for things I don’t go into in-depth. 

   


~ llh\.!,/lindsey
1Jl
wit

Who is this book for 


This book is for someone who has recently (or maybe not so recently) graduated from a bootcamp. 
This book is also for someone who started learning to code without formal education. This book is 
for a front-end web developer who knows HTML, CSS, and vanilla JavaScript. Someone who wants 
to improve their skills by making sure the code they write doesn’t exclude others. 

   


~ llh\.!,/lindsey
1Jl
wit

Why I am writing this book 


I started learning about accessibility in 2016. Or maybe it was 2015? The details are getting fuzzier 
as I get older. At the time, I was working a stint as a technical coordinator and not a web developer 
(that’s a story for another time). I spent more time helping developers and writing tickets than I 
was writing code. One day, I was assisting the Senior developer on a Drupal site for the federal 
government. The senior developer tasked me with creating accessibility tickets. I had never heard 
of accessibility before. As someone who was relatively new to web development, I looked at the 
blank ticket template like a deer in headlights. 
 
I asked my boss questions, and she told me to download the Wave tool and document all the errors 
I saw into tickets. So I did that. I literally copied and pasted the errors into tickets and put them 
into the backlog. As someone who gets anxiety when I don't know what I am talking about, I 
eventually started googling: "What is accessibility?" 
 
I can't remember the exact search results, but it was likely something from w3.org or MDN. These 
sites were telling me accessibility ensures that all sites and applications are accessible to people 
with disabilities. 
 
My desire to include others has always been pretty high on my list. But I grew up in an ableist 
system, and I never learned how disabled folks used technology. When I learned to code, I never 
came across any tutorials that talked about accessibility. Nothing that told me how this line of 
HTML would read on a screen reader. In JavaScript tutorials, they always were talking about click 
events. I never heard how someone interacted with a feature on a keyboard. I hope that this book 
helps you fill that gap, coming from someone who pieced all the information together in a way 
that made sense to me. 
 
I wanted disabled folks to have more access while using technology, not less. Technology has so 
much promise to make disabled folks have more comfortable lives. But it's moot if web developers 
ignore accessibility in the process. I didn't want to add to this problem, so I started learning more 
now that I had a "why." 
 
There's something about having a "why" that helped me navigate through the jargon. When I began 
filtering all the jargon through the lens of access, it made all that jargon less intimidating. 
 
Every blog post I write or talk I give, I filter through this same lens. In this book, I am doing the 
same. I hope that this perspective helps you as well to make the code you write more accessible. 

   


~ llh\.!,/lindsey
1Jl
wit

Goals 
After reading this book, you should:  
1. Feel confident in understanding what accessibility is 
2. Understand the common accessibility issues and low hanging fruit. 
3. Understand Accessibility APIs and when to use ARIA. 
4. Understand interactive patterns and using JavaScript to enhance not hinder accessibility. 
5. Feel confident in your testing abilities, both automated and manual. 

Thank you 
Many accessibility professionals and advocates have directly or indirectly helped me along in my 
journey, either through their advocacy or education. This is by no means a complete list. 
 
1. Marcy Sutton 
2. E.J. Mason 
3. Tatiana Mac 
4. Madalyn Parker 
5. Imani Barbarin 
6. Gregory Mansfield 
7. Matthew Cortland, Esq 
8. Leo Yockey 

   


~ llh\.!,/lindsey
wit1Jl

Part 1 - The Basics 

   


~ llh\.!,/lindsey
1Jl
wit

Chapter 1 - What is Web Accessibility 


At the most basic level, accessibility ensures that everyone, regardless of ability or disability, can 
access your web site or application. The definition is more nuanced than that, though. The Web is 
fundamentally designed to work for all people.1 However, poorly designed and inaccessible 
applications lead to broad-scale exclusion. While the Web is designed to help people at scale, it 
also can harm people at scale. 
 
I cannot write this book without discussing ableism. I am not an expert on ableism. But it would be 
ethically unsound of me to not talk about the origin of why you need to come to me for answers. A 
lot of people come to me because accessibility
​ is not the norm.​ Why? My opinion (don't come for 
me) is that we all have internalized ableism that we are fundamentally unwilling to confront. I 
have it. You have it. Our society has it. It's built into the systems that we grow up in. It's not our 
fault, but it is our responsibility. 
 
Let’s rewind a bit. What is ableism? According to Mirriam Webster, it’s “discrimination or prejudice 
against individuals with disabilities.2” But I do believe it runs deeper than that. It’s not only about 
an individual’s action. It’s the collective power of abled folks who put systems into place that harm 
disabled people. 
 
I’ve seen the systems harm me, someone who is cognitively disabled. The amount of effort and 
money it takes for me to get the medicine I need to function is absurd. The system in place is not 
friendly to a cognitively disabled person. I’ve seen the systems harm blind people in something as 
simple as adding alternative text to an image on Twitter. It was only this year (2020) that Twitter 
allowed you to add alternative text on gif images. And it was only this year (2020) that alternative 
text was enabled by default. It’s the reason that important news stories frequently don’t have close 
captioning available. 
 
Saying ableism is about individual discrimination or prejudice ignores the collective systems. I 
hope that learning how to fix some common accessibility issues and structure your applications is 
only a small part of your journey to anti-ableism. This is the part of your journey that I am more 
qualified to help you with. As a follow up to this book, I challenge you to learn more about ableism. 
You deserve to understand it more. Follow more disabled people on your social media platforms, 
especially those not in tech. Learn more about cognitive disability and chronic illness. Think 
critically about politics and how they impact the code you write. 
 
I am excited for you to join me on this journey. 
 
1
"Accessibility - W3C - World Wide Web Consortium." https://www.w3.org/standards/webdesign/accessibility.
​ ​ 
Accessed 15 Oct. 2020. 
2
​"Ableism | Definition of Ableism by Merriam-Webster." 
https://www.merriam-webster.com/dictionary/ableism.​ Accessed 15 Oct. 2020. 

10 
~ llh\.!,/lindsey
wit1Jl

Diversity of Abilities 
I am dedicating an entire section to the "Diversity of Abilities" because most people have a limited 
view of accessibility. Most people think accessibility means “It works on a screen reader.” While 
screen reader support is a vital thing to consider, accessibility includes more people than screen 
reader users. Blindness and low vision isn’t the only disability to consider. Let's widen our 
perspective. 
 
From my own experience, I got confused easily with the wealth of information. So let's break 
things down and summarize the W3C-WAI Diversity of Abilities (bit.ly/wai-abilities-barriers).
​ ​ There 
are 5 categories for the diversity of abilities: Auditory,
​ Cognitive, Physical, Speech,​ and 
3
Blindness/Low Vision.  

Auditory 
This ability regards anything that deals with hearing. This includes someone who can hear but has 
difficulty hearing with background noise. This includes someone who is hard of hearing and has 
hearing aids. That could also be someone who is hard of hearing and cannot afford hearing aids. Or 
it could be someone who is permanently deaf and needs captions or a sign language interpreter. 
 
There are a ton of potential barriers to someone who has an auditory disability. Most of those 
barriers stem from relying on auditory content without any alternatives. 
 
Let's think about the number of times we're on social media, and we come across a funny video. 
How often is there no transcript, no closed captions, or no overlay on the video from the person 
who created the video? If I share the video without context, I would be excluding many people in 
my audience. 
 
Considering how common auditory media is, not having any transcripts or captions is excluding 
people. If you’re handling your media or are a small organization with a minimal budget, there are 
plenty of tools to help you automate captions. While they aren’t best, automating and going back 
and editing is way less time consuming than creating captions yourself. 
 
If you’re a part of a larger organization, it’s worth paying someone to write captions or transcripts 
for you. Many freelancers do this full time and would do a much more efficient job at transcribing 
for you. 

Cognitive 
 

3
"Diverse Abilities and Barriers | Web Accessibility Initiative (WAI)." 
https://www.w3.org/WAI/people-use-web/abilities-barriers/.​ Accessed 15 Oct. 2020. 

11 
~ llh\.!,/lindsey
1Jl
wit

This section will get a tad personal, as I fall into this category. I have Attention Deficit 
Hyperactivity Disorder (ADHD) and Generalized anxiety disorder (GAD). Unlike other sections where 
I don’t identify, I have a personal story. 
 
I don’t like April Fools Day (April 1) for many reasons. Call me oversensitive, but I don’t think pranks 
are that funny. However, something that a lot of tech organizations do is create a “joke” site. On 
April 1, 2019, I was minding my own business and doing my day job (coding). I came across a bug 
that I didn’t understand, and as many developers would do in that situation, I googled it. 
 
To no developer’s surprise, the first thing to come up was a StackOverflow article. I clicked on it 
and started to panic. There were unicorns and stars and so much stimulus that I shut down. My 
brain can’t handle too many stimuli at once. I had to close down the web page. 
 
I posted about it on Twitter, and of course, there were a ton of reply guys in my mentions talking 
about how I could “just turn it off.” 
 
That’s the problem with our society when we talk about the issues we face as disabled people. 
When we talk about our hurdles openly, people don’t come to you with compassion or a desire to 
change. Instead, they come to you with how it's your fault. 
 
None of those reply guys considered that I​ didn't have time to look for a toggle to turn it off​ in the 
split second that I opened up the page. I had to exit out before I had a panic attack. 
 
I find that in the accessibility community, cognitive disability has often been ignored. Thankfully, 
it’s getting more attention these days. But even so, most of the standards that help with cognitive 
disabilities aren’t required of most sites. 
 
Regarding the “Diversity of Abilities,” the cognitive ability applies to a vast range of abilities. It 
applies to mental health disabilities such as anxiety, depression, or schizophrenia. It also includes 
learning disabilities, such as dyslexia. Autism Spectrum Disorder (ASD) and Attention Deficit 
Hyperactivity Disorder (ADHD) are also part of this category 
 
There are a few things we can do to improve our sites for cognitively disabled people 
● Clearly structure our content. 
● Avoid justified text and centered text. 
● Use clear fonts and avoid overly calligraphic fonts. 
● Giving multiple ways of finding content: e.g.,
​ navigation, search. 
● Provide options of how we digest the content: e.g., audio, images, diagrams, writing. 
● Limit distracting animations. 
● Give options to turn off animations quickly (assuming you don't do what StackOverflow did) 

12 
~ llh\.!,/lindsey
1Jl
wit

Physical 
Physical Disabilities can be physical conditions that need mobility assistance, such as Cerebral 
palsy or amputation. They can also be chronic pain, like Fibromyalgia or Arthritis. They can also be 
tremors and spasms. 
 
One of the best ways to help your applications be more accessible to physically disabled people is 
to ensure 100% keyboard support. This task sounds daunting, but after reading this book, I promise 
you’ll feel way less scared about it! 
 
I’ve encountered many web developers in my career who didn’t think beyond click or hover events. 
I’d ask the question to developers, “What would the user do if they couldn’t use a mouse?” Most 
times, there would be an “A-Ha” moment and a desire to learn. *Trigger
​ Warning: Ableism* 
Sometimes, I would get web developers who would say that’s an edge case. They would say, blind 
people would not need to use our application. I would look at them in horror at their ignorance, 
then go back to challenging them. *End​ Trigger Warning*​  
 
We can challenge ourselves with the simple question of “what if the user cannot use a mouse?” We 
hide so many elements from keyboard users. If they are visually abled, they'll be aware that they 
cannot access important website or application elements. I’ve seen this happen with something as 
important as the navigation to a website. 
 
In Chapter 2, we’ll talk about some of the basics to ensure Keyboard Accessibility. We’ll also talk 
about it when we talk about interactivity in Part 3. 

Speech 
Speech disabilities can range from being unable to speak, muteness, or stuttering. Many 
technologies use speech to text, such as Siri, Alexa, and Google. We have to ensure that there is a 
fallback for those that have a speech disability. Sometimes, these devices that use speech are 
enhancements on technology that didn’t use speech. For example, you can use type in Google 
search or use the microphone to Google search. 
 
That doesn’t mean we are off the hook. We have to be sure that we aren't creating features that 
can only use speech without a typing alternative. The more complex our speech technologies 
become, the more likely this could happen. One may argue that Alexa already leaves folks behind 
since you can do more than just order from Amazon on Alexa. There’s some nuance to that 
argument that I won’t get into because Alexa also helps many disabled people. The point is to 
think about these features when you’re building out your applications. Bring them up in product 
development meetings if you’re attending. 
 
Another scenario I’ve heard of is when an organization’s only option to contact them is via a phone 
line. We want to make sure that we are providing multiple ways to contact an organization. We 

13 
~ llh\.!,/lindsey
1Jl
wit

can't only provide a phone number to contact an organization. This would be a massive barrier for 
someone who had a speech disability (or a hearing disability). 

Visual 
And last but not least, visual disabilities. Visual disabilities range from vision loss, aging eyes, color 
blindness, and permanent blindness. It's wild to me how many web developers rely on visual cues 
to communicate meaning and interaction. 
 
Frequently, I see folks who use red and green to communicate meaning. Deuteranopia, or 
red-green colorblindness, is one of the most common forms of colorblindness in people assigned 
male at birth. We associate those two colors as the opposite meanings. Green is "good" or 
"continue," and red is "bad" or "stop." These opposite meanings aren't easily distinguishable to 
people with this form of colorblindness. 
 
I've also seen data visualizations that aren't accessible to blind users. "Not accessible" to blind 
users could mean multiple things. Maybe how they navigate through the data visualization is 
incomprehensible. Maybe there's no way to access the data points at all. 
 
There's also low vision people who need to be able to use zoom to read the content. But 
sometimes, when we zoom, we are unable to use all the features. 
 
These aren’t the only barriers to visually disabled people. I hope I’ve given you a taste of the 
sample of potential problems in our applications. 
 
To read more about the potential barriers for disabled people, I recommend the W3C-WAI user 
story page:​ bit.ly/wai-user-stories
​  

Demystifying WCAG 
There are a lot of different standards and laws that you’ll hear floating around. I intend to focus on 
WCAG, as that is the most universal.* 
 
One of the most intimidating parts of web accessibility was thinking I had to memorize many rules. 
You hear about these standards that HAVE to be met. When you have no idea what you’re doing 
with web accessibility, it’s hard not to freeze up and get scared. But I’m here to demystify some of 
that language for you. In this section, we’ll go over some of the frameworks of how WCAG works. 
That way, when something needs to be WCAG 2.1 AA Compliant, you’ll have a little better grasp of 
what that means. 
 
First, WCAG stands for Web Content Accessibility Guidelines. WCAG are the guidelines developed 
through the W3C process by the Accessibility Guidelines Working Group for the W3C-WAI (World 

14 
~ llh\.!,/lindsey
1Jl
wit

Wide Web Consortium - Web Accessibility Initiative). The guidelines explain how to make web 
content more accessible to disabled people. 
 
“Web Content” is a vague word. What do we mean by content? Content can be digestible by 
humans like text, media (videos, images), and sound (audio, alerts). Content can also be something 
that the browser reads, like the HTML that defines the presentation.4 
 
*I want to mention Section 508 Compliance for my US audience. I've worked in the Washington, DC 
area for all of my professional life. I hear about Section 508 Compliance often because of the 
amount of federal agency work in DC. Let’s clarify what that is. In 1998, Congress amended the 
Rehabilitation Act of 1973 to require Federal agencies to make their electronic and information 
technology (EIT) accessible to people with disabilities.5 As of the time of writing (2020), you can 
accomplish Section 508 compliance by following WCAG 2.0 Level AA Success Criteria.6 

WCAG Pour Principles 


Web Accessibility made sense to me when I learned about the POUR principles. I’ve never been a 
person who understands a lot of rules. I need to have a framework of why ​ ​ before I start digesting 
how. WCAG has a bunch of criteria that are associated with a level, which we’ll briefly talk about in 
the next section. Each criterion is filtered through one of the POUR principles: Perceivable,
​ ​ 
7
Operable,​ Understandable,
​ ​ and Robust.
​ ​ These lenses help provide clarity on why
​ ​ these criteria 
exist. 

Sites should be Perceivable


​  
For the Perceivable lens, we ensure that nothing is preventing a user from perceiving the content. 
We want to account for the many ways people interact with the Web. Information and user 
interface components must be presentable to users in ways they can perceive. 
 
We want our images to be perceivable, so we need text alternatives. Text alternatives help 
assistive devices interpret the information. Videos need close captions to be perceivable to 
someone auditorily disabled. 
 
Making our content perceivable also means structuring your markup correctly. That way, if our 
users enlarge the viewport, we don't interfere with their ability to interact with it. 
 

4
​"Web Content Accessibility Guidelines ...." ​https://www.w3.org/WAI/standards-guidelines/wcag/.​ Accessed 
15 Oct. 2020. 
5
"IT Accessibility Laws and Policies | Section508.gov." 
https://www.section508.gov/manage/laws-and-policies.​ Accessed 18 Oct. 2020. 
6
"Applicability & Conformance Requirements | Section508.gov." 
https://www.section508.gov/create/applicability-conformance.​ Accessed 18 Oct. 2020. 
7
​"Accessibility Principles | Web Accessibility Initiative (WAI) | W3C." 
https://www.w3.org/WAI/fundamentals/accessibility-principles/.​ Accessed 15 Oct. 2020. 

15 
~ llh\.!,/lindsey
1Jl
wit

Sites should be Operable


​  
For the Operable lens, we are ensuring that functionality works with a keyboard, mouse, touch 
device, or screen reader. 
 
It's always clear where someone is on the page, even if they aren't using a mouse. The keyboard 
focus is always visible. That means that we shouldn't be able to focus on links on a closed 
hamburger menu. This means we shouldn't have focus behind our pop up content. 
 
Screen readers should also be able to operate all the functionality and be told how to use the 
functionality. 

Sites should be Understandable


​  
For the Understandable lens, we are ensuring that our content doesn’t confuse our users. 
 
We also want to be clear what that user is reading by using clear headers and organization. If 
possible, we should provide multiple ways of digesting the content. We also want to use 
predictable patterns (standards) that are consistent on every page. If our users make mistakes, we 
help fix their mistakes (required forms, for example). 

Sites should be Robust


​  
I find that these particular criteria get overlooked the most often. We want to ensure that our 
content should be able to work with older and modern browsers. We also want to do the same with 
assistive devices. 
 
Note: This is a very brief summary of the POUR principles. I find that WebAIM's POUR principles 
series is helpful and clear. I recommend you read through: bit.ly/webaim-pour
​   

WCAG Levels 
One of the other main things that cause people confusion is the WCAG levels. You may hear 
something like “WCAG A” or “WCAG AA.” When folks are using this vocabulary, they are referencing 
WCAG Levels. WCAG has 3 levels of conformance with specific criteria. Each piece of criteria is 
associated with one of the POUR principles that we went over. 
 
● Level A - A specific set of 25 criteria. 
● Level AA - Level A + 13 more criteria. 
● Level AAA - Level AA + 23 more criteria.8 
 

8
"Introduction to Understanding WCAG 2.0." http://www.w3.org/TR/UNDERSTANDING-WCAG20/intro.html.
​ ​ 
Accessed 15 Oct. 2020. 

16 
~ llh\.!,/lindsey
1Jl
wit

Our goal is to reach Level AA consistently. It can be daunting and tempting to get too caught up in 
the details of “is this app this level??” I understand; I’ve been there, too. At all of the jobs I’ve been 
at where accessibility was required (which is sadly not all of them), I would get panicked questions 
from project managers or product owners who would frantically ask the team, “Are we WCAG 
AA???” Once you learn all the tactics and learn how to test, you will be way more confident about 
reaching these levels. We're going over all that in this book. 
 
If you are curious about getting into all the details, w3.org (bit.ly/wcag-quick-ref)
​ ​ has a really good 
quick reference about what criteria qualifies for each level. 
 
Note that, like a lot of things on the Web, there are versions. Often, you'll see something like WCAG 
AA, but if you see something like WCAG 2.1, that is simply just a version. Don't worry, developers, 
that doesn't mean you have to relearn a whole new set of criteria. The latest version adds ​ ​ to the 
old version, and nothing gets taken away. That is the case at the time of this writing (2020).9 

What are assistive devices and technology? 


Before we talk about making an accessible website, it's essential to understand what assistive 
devices and technologies are. Assistive devices have a broad spectrum and extend past the Web. It 
can include mobility aids, such as canes and wheelchairs. It can also include cognitive aids to help 
people with memory or attention. It can also be screen reader technology. 
 
For the web, assistive devices can look like: 
● Screen readers  
● Keyboard support 
● Speech to text 
● Screen Magnification 
● Motion tracking to help with mouse guidance 
● Head pointers to help type on a keyboard10 
 
For this book, we’ll be focusing on the mouse, screen readers and keyboards. When we have those 
supported, the APIs for these technologies work pretty well. 

   

9
​"What's New in WCAG 2.1 | Web Accessibility Initiative (WAI ...." 
https://www.w3.org/WAI/standards-guidelines/wcag/new-in-21/.​ Accessed 15 Oct. 2020. 
10
​"Types of Assistive Technology - Berkeley Web Access." 
https://webaccess.berkeley.edu/resources/assistive-technology.​ Accessed 18 Oct. 2020. 

17 
~ llh\.!,/lindsey
1Jl
wit

Chapter 2 - Common HTML Accessibility Errors (and how to Fix 


them) 
In February 2019 and February 2020, WebAIM conducted an accessibility evaluation of the top 1 
million websites’ homepages. They used their WAVE API to run the tests, which is an automated 
testing tool.11 The results were…. pretty bad. And it doesn’t even account for necessary manual 
testing. 
 
Here’s a summary of findings from February 2020: 
● 98.1% of homepages had WCAG 2.0 errors (which was an increase from the year prior) 
● 86.3% of homepages had low contrast text 
● 66.0% of homepages had missing alternative text for images 
● 59.9% of homepages had empty links 
● 53.8% of homepages had missing form input labels  
● 28.7% of homepages had empty buttons 
● 28.0% of homepages had missing document language 
 
The jarring percentages are why I say that ableism is our default, whether we want to admit to it or 
not. So many of these errors are low hanging fruit. We don’t address or even know about these 
issues because ableism is a default in our society. 
 
Let’s start to fight ableism by fighting to fix these issues on the applications we control. I’ll show 
you how to fix the most common accessibility errors in this chapter. 

2.1 Images and Alternative Text 


Why do you have alternative text? Remember when we were talking about the POUR principles? 
Alternative text makes sure that our critical images are perceivable to everyone. Every piece of 
relevant non-text content needs to have a text alternative. Most of the time, when we say that, we 
are referring to images. But this could be any media. 
 
We need to provide an alt attribute for all images. If you don't, screen readers will read whatever is 
in the src
​ ​ attribute. This includes the image extension. Can you imagine a screen reader reading 
"EizHF_cWoAArE5M.jpg"? Can you imagine if the text was even longer? Eeek! We can't avoid these 
random file paths, but we can add alternative text. 
 
There are multiple benefits of writing alternative text. The first is assistive technology reads it to 
help folks understand the purpose. Alternative text is also displayed if images are not loading. 
Lastly, it also helps provide meaning for search engines, which is good for SEO. 

11
"The WebAIM Million - An annual accessibility ... - WebAIM." 30 Mar. 2020, 
https://webaim.org/projects/million/.​ Accessed 15 Oct. 2020. 

18 
~ llh\.!,/lindsey
1Jl
wit

 
Adding alternative text seems straightforward syntactically. Just add an alt
​ ​ attribute: 
 
​ ​
<img​ ​src="./image.png" ​ ​
​ alt="some
​ descriptive text."​ /> ​
 
Easy? Right? Spoiler Alert: Not quite. We may pass an automated test when we add alternative text. 
But it doesn't always provide the best user experience. But never fear! I ask myself a few questions 
in a specific order to help me figure out what to do with alternative text. 
 
Questions I ask myself to help writing awesome alternative text: 
1. Is it redundant? 
2. Does it ​do​ something when I interact with it? 
3. What’s the context? 
 
If the answer is no to that question, then I move onto the next question. If the answer is yes, I stop 
and use the question to help me figure out what to put in my alt attribute. Let's go a bit more 
in-depth on what we do if we answer yes to any questions. 

Is it Redundant? 
What is a redundant image? Redundant images are any image that you can remove and not lose 
any context. You'd have the same access to critical information. The image is there merely as a 
visual aid. Sometimes the description or context of the image is immediately surrounding the 
image. 
 
If an image is redundant, we need an alt attribute, but the value should be ​empty.​  
 
Examples of Redundant Images 
● For your company, there’s an “Our Leadership” page with all the executives and directors. 
There are Headshots with the names of the leaders right below their image. 
● Images to help visualize the layout. e.g., spacers between content 
● Icons that don't add any semantic value - only visual. e.g., Twitter Icon with the word 
“Twitter” right next to it. 
 
When you have a redundant image, your job is done after you add an alt ​ ​ attribute and set the 
value to an empty string: 
 
​ ​
<img​ ​src="./image.png" ​ ​ ​ />
​ alt=""
​ ​

Does it do
​ ​ something when I interact with it? 
Does the image perform a function? The most common example of this is when an image is inside 
a button or a link. When your image is serving a function, describe what the function is doing. We'll 
talk more about link and button text in a later section. For the purpose here, ask if that link was 

19 
~ llh\.!,/lindsey
1Jl
wit

text and not an image; what would we use as the link text? Whatever we answer is what we'd put 
in the alt
​ ​ attribute. 
 
This is not how I would go about it: 
 
​ ​ ​
<a href="https://twitter.com/Microsoft"> ​
​ ​
​<img​ ​src="twitter.svg" ​ ​
​ ​alt="twitter icon"​ ​/>
</a>
 
The above code will read "link, image, Twitter icon." This statement doesn't tell us where we are 
going. 
 
This is much better: 
 
​ ​
<a​ ​href="https://twitter.com/Microsoft"> ​
​ ​
​<img​ ​src="twitter.svg" ​ ​
​ ​alt="Microsoft Twitter"​ ​/>
</a>
 
The above code will read "link, image, Microsoft Twitter," which gives more meaning than "Twitter 
icon." We may not even need to add Microsoft if we are on the Microsoft homepage. But I always 
like to be safe and be more specific than less. 

What’s the context? 


Let's take this image that is a screenshot of a speedometer I found on bl.ocks.org. 

 
Screenshot from http://bl.ocks.org/rmarimon/1144047
​   
 
What is the context? This is a rhetorical question because we don't know. However, if we were to 
choose this image for our content, what context is essential to know about this image? Is it 
necessary to know that there are markers every 10 units? What about the tick marks between the 
markers? Is miles per hour or kilometers per hour relevant? Is it only pertinent to know the value 
the dial is pointing at? 
 

20 
~ llh\.!,/lindsey
1Jl
wit

Any of these considerations could be important. But most importantly, ask yourself if​ it isn't 
redundant or functional, why is this image important to this page? 
 
Content Warning: Mention of Food 
Another example of context: Let's say we have a picture of a plate of food. We could describe that 
image very differently depending on what website it was on. If it's on a food blog, the alternative 
text should make you drool and want to make that dish right now! If it's on a fitness website, it's 
likely focused more on fuel for effective workouts. They're going to talk about the nutrients instead 
of making you want to drool. Or maybe they do want to make you drool and prove that healthy 
food can be tasty! It depends on the target audience of that blog. The context of the image is 
everything. 
End Content Warning 
 
Source: WebAIM Alternative Text bit.ly/webaim-alt
​  

SVG Inline Images 


I wanted to mention SVG inline images. There's a lot of cool things you can do with SVG and 
accessibility, that I could write a short book on the topic. That's not within the scope of this book. 
But if you're using inline SVGs the same way you are an <img> element, I wanted to give you some 
tactics. 
 
You cannot put an ​alt​ attribute on an SVG element and have it work the same way. You can ask 
yourself the same questions in the previous section. However, syntactically, it looks a bit different: 

If it’s redundant 
If it's redundant, we want to add an ​aria-hidden​ attribute to the SVG element and set the value 
to true. We will be talking more about ARIA attributes in part 2. But for now, understand that it'll 
serve the same purpose of an empty alt ​ ​ attribute on an <img> element. 
 
​ ​
<svg​ ​width="24" ​ ​
​ ​height="24" ​ ​viewBox="0​ ​ ​ ​
0 24 24"​ ​aria-hidden="true"> ​
<!-- svg contents -->
</svg>

If there is a text alternative 


If the inline SVG serves a function or describes something and has the same role as an <img>, you 
have to do a few things. You will want to add a role="img"
​ ​ to the SVG and the first child of the 
SVG will need to be a title element. The text content of the title will be the alternative text. 
 
​ ​
<svg​ ​role="img" ​ ​
​ ​width="24" ​ ​
​ ​height="24" ​ ​viewBox="0
​ ​ 0 24 24">​
​<title>Add
​ ​
the description here</title>
</svg>
 

21 
~ llh\.!,/lindsey
1Jl
wit

You want to add an id ​ ​ to the title and an aria-labelledby


​ ​ attribute on the SVG for older 
browsers. Again more on that in Part 2, when we talk about ARIA. 
 
<svg
​ ​
​role="img"
​ ​
​width="24"
​ ​
​height="24"
​viewBox="0 ​ ​ 0 24 24"
​ ​
​aria-labelledby="fun-image"
>
​ ​ ​
​<title id="fun-image">Add ​ ​ ​
the description here</title>
</svg>
 
Source: CSS Tricks - Accessible SVGs bit.ly/css-tricks-a11y-svgs
​  

2.2 Forms 
If I could summarize my biggest pet peeves about inaccessible forms, I would shout from the 
rooftops: “PLACEHOLDER IS NOT A LABEL.” Frequently, I see people use placeholders instead of 
having a label for a form input. But the placeholder
​ ​ attribute is not enough. It disappears as soon 
as you type in it, making it difficult for users to gut check their input. We need to label our form 
inputs and controls. We also need to associate those labels with the form inputs. 
 
There’s a lot that goes into making a form accessible, but the lack of form labels is one of the most 
common errors I see. According to the WebAIM Million, in February 2020, 53.8% of the top one 
million homepages had missing form input labels.12 Over half! And that has increased from 
February 2019. That’s why there is such a heavy focus on form labels in this section. Additionally, 
I’ll be talking about the other ways to make sure your forms are accessible. Below are all the 
considerations I make when I am creating an accessible form: 
 
● Correct form control labeling. 
● Proper semantic structure for different types of fields like checkboxes, select lists, etc. 
● Visual and non-visual indicators of form requirements 
● Clear error handling 
 
First, let’s go into how to label your forms properly. 

Proper Form Labeling Techniques 


In this section, I’ll be going over the two methods I’ve used for various situations of labeling form 
inputs: explicit labeling and implicit labeling. 

12
"The WebAIM Million - An annual accessibility ... - WebAIM." 30 Mar. 2020, 
https://webaim.org/projects/million/.​ Accessed 1 Nov. 2020. 

22 
~ llh\.!,/lindsey
1Jl
wit

Explicit labeling 
Explicit labeling is the most recommended technique. It’s got the best support with assistive 
technologies. Using this technique allows for no ambiguity to where the association is. 
 
To create an explicit label, you need to do the following: 
1. Create a label
​ ​ element with a for
​ ​ attribute. Put the label text inside the label. 
2. Create an ​input​ element with an ​id​ attribute that matches the value of the ​for.​  
 
This is what I frequently see: 

​ ​
<input​ ​name="name" ​ ​
​ type="text"
​ ​ ​
​ placeholder="Name"
​ ​ ​/>
 
Sometimes I see people try to do it by adding a label as a sibling element of the input. But they 
don’t go quite far enough to associate it. 
 

<label>Name</label> ​
​ ​
<input​ ​name="name" ​ ​
​ type="text"
​ ​ />​
 
See below for how I would explicitly label a text input. Take note that the label has a for
​ ​ attribute 
and that the input has an ​id​ attribute. The strings of those two attributes match. The ​id​ is unique 
(if you remember, in HTML and CSS classes, they’re only allowed once on a webpage). The for 
attribute tells you what
​ form input that label is for. ​ ​ 
 
​ ​ ​
<label for="name-field">Name</label> ​ ​ ​
​ ​
<input​ ​id="name-field" ​ ​
​ ​name="name" ​ ​
​ ​type="text" ​ ​/>

Implicit labeling 
The implicit labeling technique doesn’t make use of an id ​ ​ or for
​ ​ attribute. Instead, we create an 
association between the label and the input by nesting the input inside the label. In general, we 
should use this technique as a fallback for times we can’t control the attributes. 
 
1. Create a label
​ ​ element. 
2. Inside the label,
​ ​ write a text label. 
3. Also inside the ​label,​ add the ​input.​  
 
<label>
Name:
​ ​
<input ​ ​name="name" ​ ​
​ ​type="text" ​ ​/>
</label>
 
The input may also be before the label text, like for instance, in a checkbox: 

23 
~ llh\.!,/lindsey
1Jl
wit

 
<label>
​ ​
<input ​ ​name="tos" ​ ​
​ ​type="checkbox" ​ ​/>
I have read the terms and conditions.
</label>
 
These techniques are pretty straightforward. So why don’t web developers add labels if it’s pretty 
straightforward? 
 
Here’s the excuse I often hear: “It impedes the design!” I don’t like this phrasing because it implies 
we need to exclude visually disabled people from good design. But guess what? We don’t have to. 

When labels “impede” the design 


We still need to provide equal access for visually disabled users. A scenario I see often is search 
bars on the website with a magnifying glass icon. My first strategy is to use a "visually-hidden" 
class. I've also seen this class named "sr-only." 
 
Naming aside, what is the goal of this class? This class has a specific set of CSS that "clips" an 
element out of view while allowing screen readers access. But Lindsey? Why can't I say display: ​
none;​ or visibility:
​ hidden;?​ Well, fun fact, that completely hides content from ALL users. So 
all that extra effort you used to include your users would be moot. 
 
Don’t do this: 
 
​ ​
<label​ ​for="search-1" ​ ​
​ ​style="display: ​ ​ ​
none">Search</label>
​ ​
<input​ ​id="search-1" ​ ​
​ ​name="search" ​ ​
​ ​type="search" ​ ​
​ ​placeholder="Search" ​ ​/>
<input​ ​type="submit" ​ ​ ​ ​
​ value="Search"
​ ​ />

 
So what is the CSS for this mysterious class?13 
 

.visually-hidden ​ ​{
​position: ​ ​ ​absolute​ ​!important; ​
​height: ​ ​ ​1px; ​
​width: ​ ​ ​1px; ​
​overflow: ​ ​ ​hidden; ​
​clip:
​ ​ ​rect(1px, ​ ​ ​ 1px, ​ ​ 1px,
​ ​ 1px);
​ ​
​white-space: ​ nowrap;
​ ​ /*​ added line */
}
 
Now we can use that new class and add it to that label instead of adding display: ​ none;

13
"How-to: Hide content - The A11Y Project." 28 Jul. 2019, 
https://www.a11yproject.com/posts/2013-01-11-how-to-hide-content/.​ Accessed 15 Oct. 2020. 

24 
~ llh\.!,/lindsey
1Jl
wit

 
​ ​
<label​ ​for="search-1" ​ ​
​ ​class="visually-hidden">Search</label>
​ ​ ​
​ ​
<input​ ​id="search-1" ​ ​
​ ​name="search" ​ ​
​ ​type="search" ​ ​
​ ​placeholder="Search" ​ ​/>
​ ​
<input​ ​type="submit" ​ value="Search"
​ ​ ​ ​ />

 
This class isn't only helpful for form labels. It's also beneficial when more context would clutter a 
design but is necessary for screen reader users. I use this class very often, not only for visually 
hiding a form label. 
 
There's one more method that I use, but it's the last resort: Using aria-label. ​ ​ Why should this 
option be a last resort? It's only 71% reliable when used correctly. Source: Powermapper. ​ ​ 
 
How to label using aria-label: 
1. Add aria-label
​ ​ attribute to the input. 
2. Add label text as the value of the attribute 
 
​ ​
<input​ ​type="search" ​ aria-label="Search"
​ ​ ​ ​ />

​ ​
<input​ ​type="submit" ​ value="Search"
​ ​ ​ ​ />​
 
Source: W3C-WAI, Web Accessibility Tutorials - Labeling Controls bit.ly/w3c-wai-labels ​  
 
We've talked a lot about form labels. But as I mentioned in the beginning, it's not the only 
consideration for making accessible forms. Now let's move onto structuring all the different input 
types. 

Structuring Different Input Types 

Text Inputs and Text areas 


This should look the most familiar to you, as we have been using text inputs for most of the 
examples thus far. But let’s quickly go over the HTML: 
 
Text inputs have: 
● Proper form controls (what we went over in the previous section). 
● label comes before the input. 
● input has an attribute of ​type="text".​  
● input is a self-closing tag. 
 
​ ​
<label​ ​for="name-field">Name</label>
​ ​ ​
​ ​
<input​ ​id="name-field" ​ ​
​ ​name="name" ​ ​
​ ​type="text" ​ ​/>
 
Textareas 
● Proper form controls (what we went over in the previous section). 

25 
~ llh\.!,/lindsey
1Jl
wit

● id​ is on a textarea element instead of an input. 


● textarea element is not a self-closing tag. 
 
​ ​
<label​ ​for="response">Add
​ ​ ​
your additional feedback:</label>
​ ​
<textarea​ ​id="response" ​ ​
​ ​name="feedback"></textarea>

Checkboxes and Radiobuttons 


While you can have a standalone checkbox or radio button, we often use these elements as a 
grouping. I often see groups of checkboxes or radio buttons without the proper formatting. We also 
have to label the grouping, not just the form elements. 
 
Here are a few guidelines: 
 
● A ​fieldset​ element needs to wrap around groups of checkboxes or radio buttons. 
● The first child of the fieldset should be a legend ​ ​ element, which labels the group of 
checkboxes or radios. 
● For the inputs themselves, the label comes after the input. 
● The type attribute on the input takes on the value checkbox or radio. 
● Each checkbox or radio should use the same labeling strategy as we have in the previous 
examples. 
● Ungrouped checkboxes can have their unique name ​ ​ attribute. 
● Grouped checkboxes have the same name. ​ ​ 
● Grouped radio buttons have the same ​name.​  
 
<fieldset>
<legend> ​Pronouns: Select all that apply</legend> ​
<div>
​ ​
<input ​ ​id="he-him" ​ ​
​ ​name="pronouns" ​ ​
​ ​type="checkbox" ​ ​/>
​ ​
<label ​ ​for="he-him">He/Him</label>
​ ​ ​
</div>
<div>
​ ​
<input ​ ​id="she-her" ​ ​
​ ​name="pronouns" ​ ​
​ ​type="checkbox" ​ ​/>
​ ​
<label ​ ​for="she-her">She/Her</label>
​ ​ ​
</div>
<div>
​ ​
<input ​ ​id="they-them" ​ ​
​ ​name="pronouns" ​ ​
​ ​type="checkbox" ​ ​/>
​ ​
<label ​ ​for="they-them">They/Them</label>
​ ​ ​
</div>
</fieldset>
 
<fieldset>
<legend> ​Select preferred contact method</legend> ​

26 
~ llh\.!,/lindsey
wit1Jl
<div>
​ ​
<input​ ​id="email-field" ​ ​
​ ​name="contact" ​ ​
​ ​type="radio" ​ ​/>
​ ​
<label​ ​for="email-field">Email</label>
​ ​ ​
</div>
<div>
​ ​
<input​ ​id="phone-field" ​ ​
​ ​name="contact" ​ ​
​ ​type="radio" ​ ​/>
​ ​
<label​ ​for="phone-field">Phone</label>
​ ​ ​
</div>
<div>
​ ​
<input​ ​id="mail-field" ​ ​
​ ​name="contact" ​ ​
​ ​type="radio" ​ ​/>
​ ​
<label​ ​for="mail-field">Mail</label>
​ ​ ​
</div>
</fieldset>
 
Source:  
● MDN Web Docs - <input type="radio"> - bit.ly/mdn-radio
​  
● MDN Web Docs - <input type="checkbox"> bit.ly/mdn-checkbox
​  

Select Lists 
Let me get on my soapbox for a minute. I am so tired of people misunderstanding what this 
element is. I am also tired of people hacking select lists to be what they think the select list is. A 
select list is a form element that allows you to select an option. That’s it. It’s not an autocomplete 
field or a Combobox. We’ll talk about that later. 
 
A lot of people hack select lists because you can’t style the options. I agree it’s not ideal, but it’s 
not something I am willing to hack, and I discourage trying to hack it. There’s a ton of keyboard 
accessibility built into the native element. Creating a custom select list means spending time 
adding click events and customized key down events. 
 
Ok. soapbox complete. Thank you for listening. 
 
Now onto how we structure select lists: 
● Instead of associating an input with a label, we associate a select ​ ​ with a label. 
● The label has a for ​ ​ attribute that matches the select id
​ ​ attribute. 
● The select has several option ​ ​ children. 
● Each option has a value attribute. 
● You can style the select, but sadly not the options. 
 
​ ​ ​
<label for="hot-dog">Favorite​ ​ ​
hot dog topping*</label>
​ ​
<select​ ​id="hot-dog" ​ ​
​ ​name="hot-dog-topping"> ​
​ ​
<option​ ​value="mustard">Mustard</option>
​ ​ ​
​ ​
<option​ ​value="ketchup">Ketchup</option>
​ ​ ​

27 
~ llh\.!,/lindsey
1Jl
wit

​ ​
<option ​ ​value="relish">Relish</option>
​ ​ ​
</select>
*Vegan options available
 
A note about multiselect: 
You can technically add the multiple attributes on the select element. But I strongly advise that 
you don’t. If you need to select multiple options, you’re better off using a group of checkboxes. I 
recommend you don’t because it’s not the most logical pattern from both a keyboard and mouse 
user perspective. If it’s not the most straightforward pattern, it’s better to avoid it. 
 
Sources: 
● MDN Web Docs - <select>: The HTML Select element bit.ly/mdn-select
​  
● <select> your poison by Sarah Higley bit.ly/24a11y-select-p1
​   
● <select> your poison, part 2 by Sarah Higley bit.ly/24a11y-select-p2
​  

Required Fields 
We want to ensure it's obvious which fields are required to minimize the chance for form errors. We 
need both clear indicators for the required fields and proper attributes on the input. 
 
Here's my mini checklist for required fields: 
● All required fields must have a visual and semantic indicator. 
● There should be a visual indicator of which fields are required. 
● If we use a symbol like an asterisk, we should put that abbreviation at the top of the form. 
● There should be a required attribute on the input itself. 
 
Items marked with * are required.
​ ​ ​
<label for="first-name"> ​
First name
​ ​
​<span​ ​class="required">*</span>
​ ​ ​
</label>
​ ​
<input​ ​id="first-name" ​ ​
​ ​type="text" ​ ​
​ ​name="first-name" ​ ​required​ ​/>
 
Source: bit.ly/deque-required-fields
​   

Error handling 
If there is an error in client-side or server-side validation, we must do 3 things: 
1. Alert the user to the error in an accessible manner, using text and not color alone. 
2. Allow the user to fix their mistakes. 
3. Allow resubmission and revalidation. 
 
There are a few options on how to do this, which have their pros and cons. 
 

28 
~ llh\.!,/lindsey
1Jl
wit

The first option, JavaScript alert and then focus on the field that needs fixing. ​The pros of using 
this approach are that the users are immediately informed. The cons are that alerts aren't the most 
modern. Additionally, it can only evaluate one error at a time. 
 
The second option is to place all errors on the top of the form. The
​ pros of using this approach are 
that all the errors are presented together. The cons are that it may be hard for the user to 
remember each one if there are multiple errors. Every time they forget, they would have to scroll 
back up to remember. 
 
The third option is to place every error inline with the field.​ This option is my personal preference. 
The pros of using this approach are that it's obvious where every error is. The cons are that it forces 
the user to scan through the fields and look for errors. 
 
Source: WebAIM - Usable and Accessible Form Validation and Error Recovery - 
bit.ly/webaim-form-validation 

2.3 Color 
Color contrast, as of this writing, is the biggest issue on the WebAIM One Million. 86.3% of the top 
one million websites’ homepages had color contrast issues.14 This statistic is interesting to me as 
it's one of the most straightforward errors to fix, but most people are resistant to fixing. I believe 
the reason most people don't improve their color contrast is attachment to brand styles. 
 
Improper color contrast impacts colorblind people, low vision people, and people with aging or 
strained eyes. I live by 2 primary guidelines when it comes to color. First, don't rely on color to 
communicate important information. Second, check the color contrast on everything that needs to 
be checked. 
 
The biggest offender of the first guideline is data visualizations. We should ask designers about 
shapes or patterns to inclusively communicate the data. Additionally, everyone perceives color 
differently. Colors in different cultures have different meanings. The other time I've seen this 
happen is when people try to perform form validation and point out form errors with a red border. 
Please use words to communicate the mistakes. 
 
For the second guideline, I recommend to contrast check everything that isn't black (#000000) and 
white (#ffffff). I've had people ask me if "this color contrast is accessible." Most of the time, when 
they are asking me, it isn't glaringly obvious. I need to use a contrast checker, which is something 
that can be done by anyone. It's better not to trust your biases about good enough color contrast 
and use a handy tool to check. The essential ratios to remember for WCAG AA (the minimum) is 3:1 
for large text and 4.5:1 for normal text. 

14
"The WebAIM Million - An annual accessibility ... - WebAIM." 30 Mar. 2020, 
https://webaim.org/projects/million/.​ Accessed 1 Nov. 2020. 

29 
~ llh\.!,/lindsey
1Jl
wit

Text contrast 
When I say the text contrast ratio, I mean the text (sometimes referred to as foreground) color 
against the background color or vice versa. Let's go over "large text" and "normal text" in more 
specific terms. Again, we should use standards instead of our brains and bias to discern large text 
and small text. 
 
"Large text" is can be one of two things 
● Text with regular font-weight and has a font-size of 24px or greater. 
● Text with bold font-weight and has a font-size of 18.66px or larger. 
 
Instead of saying "Normal text" is smaller than large text, let’s spell it out. “Normal text” is one of 
two thing: 
● Text with regular font-weight and has a font-size smaller than 24px. 
● Text with bold font-weight and has a font-size smaller than 18.66px. 
 
There are exceptions to these contrast rules. They are: 
● Inactive text. For example, a disabled button. 
● Offscreen elements. For example, a hamburger menu that animates in. Once it is visible, it 
does need to have a proper color contrast. 
● Decorative text that is not meant to be read. 
● Text within a logo. 
 
Besides text contrast, we need to consider a few other areas for color contrast. Let's go over what 
those are. 

UI Components & Graphical Objects 


UI components and Graphical Objects both require a contrast ratio of 3:1.
​ UI
​ Components are 
non-text elements that are controls, usually in the form of text inputs or form elements. We should 
strive to make sure that those elements also have accessible focus states. These focus states 
should contrast their background as well. Graphical Objects are non-text elements that are 
required to understand the content. e.g., an Icon without descriptive text around it. 

Links 
The contrast rule applies when we have a link inline with text. We measure the contrast ratio 
between the link color and the text color. The text color and the link color should have a contrast 
of 3:1. There also must be a visual non-color change when we hover or focus on the link (like 
adding an underline). Ideally, links should also have a non-color differentiation between text and 
links in its default state. It doesn't have to be the default text underline. You can get creative. 
 
Sources 

30 
~ llh\.!,/lindsey
1Jl
wit

● WebAIM - Contrast Checker bit.ly/webaim-color-checker


​  
● WebAIM - Contrast and Color Accessibility bit.ly/webaim-color-contrast
​  

2.4 Buttons & Links 


Clearly soapboxes are a theme in my book, so here’s another one. My biggest pet peeve in web 
development is when people hack links because they don't like button styling. People assume that 
just because a link is also focuasble, it’s no problem at all. But that’s not true and we shouldn’t mix 
the two up.  

Difference between Links and Buttons 


Lucky for you, I have a simple cheat sheet for you about whether to use buttons or links. Ready? 
 
Buttons do things. 
 
Links go places. 
 
�  
 
It's tempting to use a link when you want a clickable and focusable element without button styling. 
Sometimes I appreciate the thought beyond clicking. But I'm here to remind you that​ functionally, 
buttons don't work the same way as links.​ Let's return to what the differences between links and 
buttons are. 
 
In HTML, links use an a ​ ​ tag with an href
​ ​ attribute. That attribute contains a value that creates a 
link to web pages. It can also contain files, emails, and internal links within the page. And while it 
is focusable, on a keyboard it only triggers a click on keydown Enter. 
 
Buttons are a clickable element that can perform actions within the page. These actions could be 
submitting a form, opening a modal, or opening an accordion panel. When you interact with a 
keyboard button, it triggers a click on keydown Enter and Space. 
 
When I say "triggers a click," I mean that we don't have to write separate keydown handlers. 
Buttons and links have these handlers in their native functionality. For buttons, in particular, we 
are usually using JavaScript to create interactions. The click event also encompasses when we 
focus on the element and press the appropriate keys. 
 
Another cheatsheet: 
● If you can't think of an href value other than #, then it's a button.* 
● If you need a click event listener for it, then it's a button. 
 

31 
~ llh\.!,/lindsey
1Jl
wit

*The only exception is if you’re prototyping a navigation and that’s not the final link value, but 
even then I’ve seen people forget to replace these 

Empty Links and Buttons 


Yay! We know the difference between a link and a button. Now we can cover another related but 
common accessibility issue. Another big offender is empty links and buttons. 
 
I’ve seen stuff like this: 
 
<button></button>
 
I’ve seen empty buttons where the button has a background image set in CSS. But there's no 
context for assistive technology to use. On a screen reader, that would say "button." 
 
Sometimes I also see images without alternative text as links or buttons. Because the button has a 
child element, it isn't technically empty. However, it won't necessarily sound pleasant. 
 
​ ​
<a​ ​href="https://www.google.com/"> ​
​ ​
<img​ ​src="image.png" ​ ​/>
</a>
 
This would read “link, h-t-t-p-s-colon-slash-slash-w-w-w-dot-google-dot-com-slash.” 

Click where? 
Often screen reader users will navigate a site using available links and buttons. But if you are a 
blind user and using this technique, you aren’t getting the surrounding context. When a link says, 
“Click here,” I can imagine myself saying, “click where?” Where are we going? What are we doing? 
That’s why it’s important to add context to the link or button. Powerful call to action links and 
buttons are way more effective and clear to everyone. 
 
Additional Reading: 
● MDN - The Anchor element bit.ly/mdn-anchor-tag
​  
● MDN - The Button element bit.ly/mdc-button
​   
● Marcy Sutton - Links vs. Buttons in Modern Web Applications bit.ly/buttons-vs-links
​  
● WebAIM - Links and Hypertext bit.ly/webaim-links
​  

2.5 Heading Structure 


I like to think of heading structure as a book or an outline. There's a title; there are parts; there are 
chapters. Sometimes there are subsections in those chapters. Sometimes it gets even deeper than 
that.  
 

32 
~ llh\.!,/lindsey
1Jl
wit

Imagine this scenario: We open up a romance book. The first page is the title of the book. You turn 
the page. Next thing you know, you have a small heading about Michael wooing their love interest 
Chloe. You have no idea who these characters are; you started in the middle of the book. You can't 
read the book's title then go straight to a subsection of a chapter without introducing the chapter 
first. 
 
Frequently, screen reader users use headings to navigate through a page. They can get a glance at 
a section before they jump to it. If we skip a heading, it gets confusing for someone to understand 
where that section belongs.  
 
We should keep this structure: h1 > h2 > h3 > h4 > h5 > h6. 
 
You can jump back up as many levels as you need because you can move onto the next chapter 
after you finish a subsection. 
 
Here are the ground rules: 
● There is only one h1 per page (one title of a book). 
● Don't jump heading levels (no introducing subsections without introducing the chapter). 
● Don't default to using the headings for text size if it doesn't have the associated meaning. 
 
Don’t do this: 
 

<h1>Title</h1> ​
<h3>Some ​ text that's not a heading, but we want "bigger" text</h3> ​
 
Do this: 
 

<h1>Title</h1> ​
<h2>Chapter ​ ​
1</h2>
<h3>Some ​ ​
sub section</h3>
<h2>Chapter ​ ​
2</h2>
 
Sources: 
● bit.ly/wai-headings 
● bit.ly/yale-headings 
 
Because headings are semantic HTML, it's a great time to talk about semantic HTML. 

2.6 Overview of Semantic HTML 


Because we just talked about heading structure, it’s important to briefly go over Semantic HTML. 
Disclaimer: This is an overview.
​ ​ It wouldn’t be the most effective use of your time to go over every 

33 
~ llh\.!,/lindsey
1Jl
wit

single semantic HTML element, but I wanted to go over why semantic HTML is better than non 
semantic HTML. The short version of this section is this: 
 
Use a semantic element over a non semantic element if applicable 
● A ​header​ element is better than a ​div.​  
● A ​main​ element is better than a ​div.​  
● A ​footer​ element is better than a ​div.​  
● A ​section​ element is better than a ​div.​  
● A ​button​ element is better than a ​span.​  
 
Basically, anything is better than a div​ ​ unless that element really doesn’t have any semantic 
meaning to it. 
 
Semantic HTML gives an assistive technology user a lot of information about the page for free 
without you having to build it in yourself. 
 
Regions​ (also referred to as landmarks) describe regions of the page like header, navigation, footer, 
main, aside. Headings
​ organize,
​ categorize, and rank content. Content
​ structure​ allows you to 
discern different content formats like lists and paragraphs. 
 
To demonstrate why semantic HTML is helpful, let’s talk a bit more about headings, since we just 
went over that in the last section. Take this markup 
 
<h1>Title ​ ​
of the Page</h1>

<p>Here's a little bit of information before we get started</p> ​
<h2>Hey ​ ​
you, heading 2</h2>

<p>Here's a little bit of info about the second heading</p> ​
<h2>Here's ​ another heading for testing purposes</h2> ​

<p>Here's a little bit of info about the second heading</p> ​
 
I can use the command on VoiceOver control + option (also known as VO if I ever say that) + 
command + H and navigate through these headings relatively easily 
 

34 
!Title of the Page
Here's a little bit of information before we get started

Hey you, heading 2


Here's a little bit of info about the second heading

Here's another heading for testing purposes


Here's a lilt.lebit of info about the second heading

x First heading, heading level 1 Title


of the Page

Title of the Page


Here's a little bit of information before we get started

IHey you, heading 2


Here's a linJe bit of info about the second heading

Here's another heading for testing purposes


Here's a little bit of info about the second heading

If I had something like this: 


 

35 
~ llh\.!,/lindsey
1Jl
wit

​ ​
<div​ ​class="title">Title
​ ​ ​
of the Page</div>

<p>Here's a little bit of information before we get started</p> ​
​ ​
<div​ ​class="heading">Hey ​ ​ ​
you, heading 2</div>

<p>Here's a little bit of info about the second heading</p> ​
​ ​
<div​ ​class="heading">Here's ​ ​ another heading for testing purposes</div> ​

<p>Here's a little bit of info about the second heading</p> ​
 
I wouldn’t be able to use this command to get access to these headings.  
 
Let’s put this into perspective. You’re reading this book, and I would suspect that there’s a fair 
likelihood that you read my blog and other blogs. Let’s just say you were having a brain fart about 
something very basic web development related and you went to a blog post that was way more 
comprehensive than you needed. So what would you do? You’d browse the heading levels until you 
found the section that addressed where you were having a brain fart. Imagine not having an option 
to quickly browse through the content? 
 
That’s why semantic HTML is so important  
 
Sources and resources: 
● MDN Web Docs - Semantics bit.ly/mdn-glossary-semantics
​  
● MDN Web Docs - HTML: A good basis for accessibility bit.ly/mdn-a11y-html
​  
● WebAIM - Semantic Structure: Regions, Headings, and Lists bit.ly/webaim-semantics
​  
● W3C-WAI Tutorials - Page Regions bit.ly/w3c-regions
​  
● W3C-WAI Tutorials - Headings bit.ly/w3c-headings
​  
● W3C-WAI Tutorials - Content Structure bit.ly/w3c-content-structure
​  

2.7 Focus States & Tabindex 


We haven't gone over what it means when something is "focusable." An element is focusable when 
you press the tab key, and then you see an outline on that element. That means you can interact 
with the element on a keyboard. By default, links, form inputs, and buttons are focusable. They also 
have built-in interactions with the keyboard. 
 
I often observe developers removing the focus states altogether. All browsers provide a default 
styling for the :focus
​ ​ pseudo-class. These focus states help a keyboard user see when they can 
access focusable elements. Removing the default outline takes away this accessibility feature. If 
you are going to remove it, have some fun and play with some custom styling. If you find the focus 
styling ugly, then make it pretty! 
 
Don't do this: 
 

a:focus ​ ​{
​outline: ​ none;
​ ​

36 
~ llh\.!,/lindsey
1Jl
wit

}
 
Unless you're going to add some fun styling: 
 

a:focus ​ {
​outline: ​ none;
​ ​
​box-shadow: ​ 0
​ 0 0 3px #45008e; ​
}
 
When you make an element focusable, interactions won't happen by default. Now when you're 
focused on them, they can listen for key events. It's important to note that screen readers don't 
need to be focusable to read the content. The only time screen readers won't read the content is if 
you add display:
​ none​ or visibility:
​ hidden​ in your CSS. For sighted keyboard users, 
interactive controls must be focusable for it to be accessible. It's important to communicate where 
they can interact with elements. 
 
What about elements that aren't focusable by default? Can you make them focusable and 
operable? 
 
Yes, you can do so with the tabindex attribute. Before adding this attribute to any element, I ask 
myself if I can use an element that's focusable by default. However, sometimes we have JavaScript 
patterns that call for adding tabindex to elements. So it's important to know what it means and 
how to do so appropriately. 
 
Except for links, buttons, and form controls, you need to add the tabindex attribute to give the 
ability to focus on an element. The value it takes is typically -1 or 0. When you set a tabindex to 0, 
you are not only adding the ability to focus on that element, but you're also adding it to the tab 
order. 
 
What is the tab order? Tab order is the order you'll see focusable elements when you press the tab 
key. When you add tabindex as 0, the tab order follows the order of what's in the DOM. I strongly 
recommend against using a tabindex greater than 0. If you do, you have to manage the tab order 
for the entire site, which opens up your margin for error exponentially. 
 
Sometimes, you want something to be focusable, but not in the tab order. For example, if you're 
going to send focus to a wrapper with JavaScript, but otherwise, you wouldn't interact with that 
wrapper element. To do this, you can set the tabindex to -1. 
 
Sources: 
● MDN - tabindex bit.ly/mdn-tabindex
​   
● WebAIM - Keyboard Accessibility, Tabindex bit.ly/webaim-tabindex
​  

37 
~ llh\.!,/lindsey
1Jl
wit

2.8 Skip Links 


Skip links are anchor tags that link to internal landmarks on the page to help you skip a bunch of 
redundant links. Let's present you with a scenario. You come to a site, and you're a keyboard user. 
You tab through the navigation, and you find the link you want to visit. Then you go to the page, 
and then you have to go through the navigation again?? That's annoying, you already did this, and 
you want to go to the content. Enter, skip links. 
 
How to create Navigation Skip Links 
1. Create an id on the element where the content lives, ideally the main
​ ​ element 
2. Create an anchor tag right after the opening body tag and set the href
​ ​ to be the #id
​ ​ of the 
main element 
3. Visually hide the link and show it on focus 
 
You can add multiple skip links where you have other navigations before important content. That's 
not always necessary. But you MUST have one as the first element in your body tag. 
 
<body>
​ ​
​<a href="#main">Skip
​ ​ ​ to Main Content</a> ​
<nav>
​<!-- Nav Links here-->
</nav>
​ ​
<main ​ ​id="main"> ​
<!-- Content Here -->
</main>
</body>
 
Source: WebAIM - "Skip Navigation" Links bit.ly/webaim-skip-links
​  

2.9 Tables 
Properly formatted data tables are the most underrated accessibility feature. It's possible to make 
many data visualizations accessible, which I won't cover in this book. However, we should give our 
user an alternative way to access the data points. Sometimes a user doesn’t necessarily want to 
navigate through a data visualization.  
 
A Data Table creates a grid with data that shows visual associations. The structure can help sighted 
users understand data relationships. But what about someone who is visually disabled? Proper 
markup allows the user to get clear notifications about where the data cells belong. 
 
Rules 
● The outer wrapper of a table should always be a table
​ ​ element. 

38 
~ llh\.!,/lindsey
1Jl
wit

● If there is a description before or after the table, wrap that in a caption


​ ​ element inside the 
table. 
● All tables must have table headers for columns and/or rows using the th ​ ​ element. 
● Use the scope
​ ​ attribute on th
​ ​ to designate if it's a column header or a row header. 
● th​ cannot be empty, but td ​ ​ can. 
● thead,​ tbody,
​ ​ and tfoot
​ ​ don't add accessibility features but are nice to have when your 
tables get advanced. 
 
A one directional table is when the th‘s ​ ​ are only going in one direction. You can identify that 
direction using the scope ​ ​ attribute. You can see an example below of how I would mark this up. 
 
<table>
<tr>
​ ​
<th ​ ​scope="col">First ​ ​ Name</th> ​
​ ​
<th ​scope="col">Last ​ ​ ​
Name</th>
</tr>
<tr>
<td> ​Lindsey</td> ​
<td> ​Kopacz</td> ​
</tr>
</table>
 
 
A two directional table is when there are th‘s ​ ​ for both columns and rows. See below for how I 
would mark this up. I took the same table as before, but made my name as the table row. 
 
<table>
<tr>
​ ​
<th ​ ​scope="col">Name</th> ​ ​ ​
​ ​
<th ​ ​scope="col">Fave ​ ​ Programming Lang</th> ​
​ ​
<th ​ ​scope="col">Job ​ ​ Title</th> ​
</tr>
<tr>
​ ​
<th ​ ​scope="row">Lindsey</td> ​ ​ ​
<td> ​JavaScript</td> ​
<td> ​Accessibility Lead</td> ​
</tr>
</table>
 
A two directional table can be really helpful when the data starts to get very complex.  
 
Source: WebAIM - Creating Accessible Tables, Data Tables bit.ly/webaim-data-tables​  

39 
~ llh\.!,/lindsey
1Jl
wit

2.10 Languages 
Why is language important for accessibility? We want to ensure screen readers are reading in the 
appropriate language and dialect. All you have to do is add a lang attribute to the HTML element. 
 
​ ​
<html​ ​lang="en"> ​
​<!-- All the HTML -->
</html>
 
I'm in the United States, as is a large part of my audience. Web development documentation and 
products tend to focus on English speakers. However, we must think more extensively than the 
web developer community. It's important to remember that disabled users exist worldwide, and 
many of them don't speak English. Imagine having a screen reader see French and reading them 
with English pronunciation. It would sound very gnarly, and I'm sure every French speaker would 
cringe. 
 
Additionally, you can add the lang attribute on other elements within the text. If you are language 
switching in your content, the screen reader will be sure to change its pronunciation. 
 
​ ​
<html​ ​lang="en"> ​
<head></head>
<body>
<p>
Hey! I'm going to teach you how to say "My name is Lindsey" in French.
​</p>
​ ​
<p ​ ​lang="fr">Je ​ ​ m'appelle Lindsey.</p> ​
</body>
</html>
 
Sometimes it's helpful to add country-specific languages as well, to account for dialects. For 
example, British English (en-GB) and American English (en-US). Or Castilian Spanish (es-ES) and 
Mexican Spanish (es-MX). 
 
Below is a list of country codes and language codes: 
● w3schools - HTML ISO Country Codes Reference bit.ly/w3schools-country-codes
​  
● w3schools - HTML Language Code Reference bit.ly/w3schools-language-codes

   

40 
~ llh\.!,/lindsey
wit1Jl

Part 2 - ARIA 

   

41 
~ llh\.!,/lindsey
1Jl
wit

Chapter 3 - What is ARIA? 


Oh, ARIA, one of my favorite topics. Before I go into what ARIA is, I want to clarify something 
because I am well aware that I sound like I am bashing on ARIA. I​ actually like ARIA.​ It's helpful to 
enhance some of our accessibility features. But enhance
​ ​ is the important word here. Many people 
jump into ARIA and think it will solve all their problems. They don't consider the impact. I 
appreciate that people are thinking about accessibility. But the intent doesn't matter if the user 
experience is worse. I always recommend when using ARIA, proceed with caution.  
 
OK, so what is ARIA? 
 
Accessible Rich Internet Applications - (ARIA) defines ways to make Web content and Web 
applications (especially those developed with AJAX and JavaScript) more accessible to people with 
disabilities.15 So, what does that really mean? ARIA helps add more context to your content. It's 
important to remember that it's only helpful if you need the context. A common mistake when 
learning about accessibility is jumping into ARIA before learning semantic HTML. Using ARIA 
before using semantic HTML can have a detrimental impact if you don't know what you're doing. If 
you don't use ARIA carefully, you can make the user experience less accessible, not more. In a 
world where the default is to be ableist, the impact is more important than the intent.  
 
ARIA can be intimidating because it's so vast. There are SO MANY attributes. Honestly, I don't even 
bother trying to remember them all except for a few key ones (which we'll go over in Chapter 5). 
For me, what's more helpful is putting it into practice and understanding what's happening by 
listening to the differences on a screen reader. We are going to jump straight to the code before we 
learn all those attributes.  
 
In Chapter 2, we talked about the difference between links and buttons. Let's look at how I've seen 
many people create a "button." 
 
<!-- Example 1 -->
​ ​
<div​ ​role="button">Menu</div>
​ ​ ​
<!-- Example 2 -->
​ ​
<a​ ​role="button" ​ ​
​ ​href="#">Menu</a>
​ ​ ​
<!-- Example 3 -->
​ ​
<button>Menu</button>
 
The "role" attribute is one of the most popular ARIA attributes. We'll go more in-depth about what 
that means in the next couple of chapters. The important takeaway is it changes what assistive 
devices read for the element. Let's go over what the differences in these three examples are: 

"WAI-ARIA Overview - World Wide Web ...." https://www.w3.org/WAI/standards-guidelines/aria/.


15
​ ​ Accessed 1 
Nov. 2020. 

42 
~ llh\.!,/lindsey
1Jl
wit

 
​ ​
<div​ ​role="button">Menu</div>
​ ​ ​
 
When you use your tab key, you cannot access this div.
​ ​ 
This element announces "Menu, Button" on VoiceOver, but then you cannot interact with it. 
 
​ ​
<a​ ​role="button" ​ ​
​ ​href="#">Menu</a>
​ ​ ​
 
With this example, you can access it with the tab key when the screen reader is off. It activates on 
the "Enter" key. This element announces "Menu, Button" on VoiceOver. 
 
​ ​
<button>Menu</button>

With this example, you can access it with the tab key when the screen reader is off. It activates on 
the "Enter" or "Space" key. This element announces "Menu, Button" on VoiceOver. 
 
Why is this important to point out? ARIA doesn't change any functionality. It only changes how 
screen readers announce elements. The link functionality didn't change. It doesn't suddenly work 
on the Space key because we changed the role. We'd have to add more JavaScript to make it work 
the same way. It's critical to use these attributes responsibly. We should use ARIA to enhance 
already semantic HTML. Occasionally, a semantic version of HTML doesn't exist. But a majority of 
the time, there is a semantic version. 
 
So, what is the role? What does this change? To understand this, let's get deeper into the 
accessibility tree and accessibility object model (AOM). 

   

43 
~ llh\.!,/lindsey
1Jl
wit

Chapter 4 - The Accessibility Tree & AOM 


Like how the DOM tree16 contains a tree of objects with data about the markup, the Accessibility 
Object Model (AOM) contains accessibility data that assistive tech can use. There's a substantial 
technical spec, but because these things can be overwhelming, let's keep it simple and practical for 
our understanding: 
 
1. Browsers convert markup to a DOM tree. 
2. Browsers then take all the info from the DOM tree and create an accessibility tree. 
 
If you'd like to read more about this in plain language, the MDN website helps me a lot: 
bit.ly/the-aom 
 
There are four primary properties in the accessibility tree object: name, description, role, and state. 
I like to show rather than tell, so let's get started with some examples. But first, I need to briefly 
show you the Accessibility Developer Tools I will be using. 
 
To show this to you, I will be using Chrome Developer Tools* bit.ly/chrme-dev-tools.
​ ​ To open up 
the Accessibility Developer Tools in Chrome: 
1. Inspect an element by right-clicking on the element you want to see the accessibility 
properties for and select "Inspect."  
2. In the same area that you see the Styles, there are other options like "Computed" and 
"Event Listeners." In that section, you should see "Accessibility."  
3. Select that option. 
4. Additionally, you can also open the element inspector with Command​ + Option + C​ for Mac 
users and Control
​ + Shift + C​ for PC users. bit.ly/chrome-dev-shortcuts
​  
 
*I have Chrome Version 86.0.4240.75 as of this writing 
 
If you are using Firefox developer tools* (bit.ly/ff-devtools):
​ ​  
1. Right-click on an element 
2. Select "Inspect Accessibility Properties."  
3. Additionally, you can open up the element inspector using Command
​ + Option + i​ on a mac 
Control + Shift + i ​ on a PC. Then you can go to the "Accessibility" section on the toolbar 
and explore the tree. I find the first method the quickest.  
 
*I have Firefox version 81.0.1 (64-bit) 
 

16
"Introduction to the DOM - Web APIs - MDN ...." 22 Sep. 2020, 
https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction.​ Accessed 1 Nov. 
2020. 

44 
~ llh\.!,/lindsey
1Jl
wit

You can read more about the shortcuts for Firefox: bit.ly/ff-dev-tool-shortcuts.
​ ​ Read more about 
accessibility dev tools: bit.ly/ff-a11y-inspect.
​ ​ 
 
The Firefox accessibility tools give very similar information to Chrome Developer Tools. It may look 
slightly different and have different naming conventions. I'll be sure to share screenshots in Firefox 
as well, but I'll spend most of my focus on Chrome Developer Tools. 
 
If you're on a computer reading, you can follow along with these examples on Codepen: 
bit.ly/checkbox-example  
 
Consider a checkbox: 
 
​ ​ ​
<input type="checkbox" ​ ​ ​
​ id="chk-label" ​ />

​ ​
<label​ ​for="chk-label">Remember
​ ​ ​
my preferences</label>
 
This input has all sorts of accessibility data from the type attribute and having an associated label. 
If we go to that input and inspect the accessibility properties, we'll see something that looks like 
this: 
 
Chrome: 
• Accessibility Tree
v WebArea "CodePen - Checkbox example"
v generic
checkbox "Remember my preferences"
• ARIA Attributes

No ARIA attributes

• Computed Properties
• Name: "Remember my preferences"
aria- label led by: Not specified
aria- label: Not specified
From label (for): label "Remember my preferences"
Contents: Not specified
tit le: Not specified
Role: checkbox
Invalid user entry: false
Focusable: true
Checked: false
Labeled by: label
 

45 
 
Firefox: 
Cf O Inspector ID Console D Debugger {} Style Editor (71 Performance ir Accessibility » o]
Check for issues: None ~

Role Name • Properties


.,..document: "CodePen - Checkbox name: "Remember my preferences"
example" role: "checkbutton"
.,.. section:
► actions: [ ...]
checkbutton: "Remember my preferences" value: 1111

text leaf: DOMNode: input#chk-label -¢­


► label: "Remember my preferences" description: ""
► section: keyboardShortcut: ""
childCount: 0
indexlnParent: 0
• states: [ ...]
0: "focusable"
1: "checkable"
2: "opaque"
3: "enabled"
4: "sensitive"
length: 5
► relations: { ...}

A few key takeaways about the properties 


● roles: checkbox (in Firefox, it says checkbutton) 
● state: checked: false, focusable: true 
● label: "Remember my preferences." 
 
All these properties come by default with the semantic HTML. You don't have to add any ​ ​ ARIA 
attributes for the accessibility tree.  
 
You could (but please please don’t) do this: 
 
​ ​
<span​ ​role="checkbox" ​ ​
​ ​aria-checked="false" ​ ​
​ ​tabindex="0"
​ ​
aria-labelledby="chk1-label"></span> ​
​ ​
<span​ ​id="chk1-label">Remember
​ ​ ​
my preferences</span>
 
If you take a look at the accessibility tree on this markup, it looks remarkably similar. 
 
 
In Chrome: 

46 
. -

Styles Computed Event Listeners DOM Breakpoints Properties Accessibility

.., Accessibility Tree


v WebArea "CodePen - Checkbox example"
v generic
checkbox "Remember my preferences"
.., ARIA Attributes
role: checkbox
aria-checked: false
aria-labelledby:chkl-label

.., Computed Properties


"' Name: "Remember my preferences"
"'aria-labelledby:
la be l#c hkl- label" Remember my preferences"
aria-label: Not specified
Contents: Not specified
tit le: Not specified
Role: checkbox
Focusable: true
Checked: fa ls c
Labeled by: label#chkl-label

 
In Firefox:
• • Developer Tools - CodePen - Checkbox example - https://cdpn.io/littlekope0903/debug/rNxEyBm/DqkDdgO ...
(i O Inspector [) Console D Debugger {} Style Editor (j) Performance ,r Accessibility » oJ •••
Check for issues: None :

Role Name • Properties


• document: "CodePen - Checkbox name: "Remember my preferences"
example"
role: "checkbutton"
• section:
► actions: [ ...]
checkbutton: "Remember my preferences"
value: 1111

text leaf:
DOMNode: span (>­
• label: "Remember my preferences"
description: ""
text leaf: "Remember my preferences"
keyboardShortcut: ""
• section:
childCount: 0
checkbutton: "Remember my preferences"
indexlnParent: 0
► label: "Remember my preferences"
• states: [ ...]
0: "focusable"
1: "checkable"
2: "opaque"
3: "enabled"
4: "sensitive"
length: 5
► relations: { ...}
 
 

47 
~ llh\.!,/lindsey
1Jl
wit

But Lindsey, all the essential stuff looks the same. Why are you saying I shouldn't do this? 
 
Well lots of reason, and here's a few: 
● Semantic HTML is more straightforward and needs fewer attributes. There are so many 
different attributes that we now are responsible for.  
● It's harder to read. 
● Adding attributes doesn't replicate native functionality. You have to create a keydown or 
keyup event listener, check if the key is a space (how you interact with checkboxes), and 
then toggle the checked state. Additionally, you have to create a click event listener to 
toggle the checked state. input[type="checkbox"]
​ ​ has that behavior by default without 
any JavaScript.​  
● When you add more JavaScript, you are writing more tests. I actually love writing tests, but 
only when they are necessary. Custom functionality for something that is a native element 
is redundant. 
● ARIA can be inconsistent among operating systems and browsers. Native elements have 
way more consistent patterns.  
 
With that being said, let’s move onto some of the most common ARIA attributes and how they are 
used. 
 
 

   

48 
~ llh\.!,/lindsey
1Jl
wit

Chapter 5 - When to Use ARIA & Most Useful ARIA Attributes 


So, at the beginning of Part 2, I mentioned that I don't hate ARIA. But in the past two chapters, I 
was advocating for not using it. So when do you use it? I have a few questions I ask myself when I 
decide whether I use ARIA. 
 
First, is there a semantic option supported by browsers?​ If yes, I use that first. If not, I look into 
ARIA. One example where I don't use semantic HTML (yet) and use ARIA is creating an accordion 
(which we will go over in Part 3). There is a details element (bit.ly/html-a11y-details).
​ ​ However, it's 
not supported in IE. Yes, yes, I know. Who cares about IE. Well, that may depend on your users. My 
current job requires my team to support IE11, so using the details element would not be an option. 
 
Second, does the semantic HTML need some more context?​ There are a few attributes we can't 
replicate using Semantic HTML. The semantic HTML needs something a little extra to enhance the 
user experience. Often, we convey the meaning and context through visual cues. Using ARIA can 
help add that context back for screen reader users. 
 
I can sum up my feelings with this metaphor that I am very proud of. Semantic HTML is the salt 
and pepper; ARIA attributes are all the other spices. Salt and pepper are usually all that's needed to 
make something tasty. However, some dishes need a little extra kick. But you must add spices 
carefully and with intention. Adding more spices to the mix sometimes makes it more complex and 
more challenging to balance the flavor. You have to know what you're doing to make something 
good. 
 
Now that I've talked about ARIA, let's talk about the ARIA attributes that I often use and find the 
most helpful. 

role 
As we showed before, every semantic element has an inherent role. Using the role attribute is a 
way to change the role in the Accessibility Tree. You can only use specific predefined values for the 
role. You can't make up values, sorry I don't make the rules �

 
Examples: 
● button 
● tooltip 
● navigation 
● dialog 
● slider 
● progressbar 
● heading 
● list 

49 
~ llh\.!,/lindsey
1Jl
wit

● listitem 
● status 
● alert 
 
This list is not exhaustive, but it is a list of the ones I see the most often. For more thorough lists, 
you can check out MDN ​bit.ly/a11y-aria-roles​ or W3C ​bit.ly/w3c-role-def.​  
 
Below is an example of using the list ​ ​ and listitem
​ ​ role to create a list out of divs (again, I 
would rather you use an ​ol​ or ​ul,​ but this is technically valid markup) 
 
​ ​
<div​ ​role="list"> ​
​ ​
<div​ ​role="listitem">List ​ ​ ​
item 1</div>
​ ​
<div​ ​role="listitem">List ​ ​ item 2</div>​
​ ​
<div​ ​role="listitem">List ​ ​ item 3</div> ​
</div>

aria-labelledby 
The ​aria-labelledby​ attribute identifies the element that ​labels​ the current element. This 
attribute's value takes on a string that matches the id ​ ​ of the element used to label it. So it's 
labeled by​ an element with the same ID. What's important to note here is that it announces ​before 
the associated element. The text value of the label tends to be brief. Remember that labels are not 
long passages of text. This technique is often an alternative to using the for ​ ​ attribute with labels. 
Like this cringey example from the last chapter: 
 
​ ​
<span​ ​role="checkbox" ​ ​aria-checked="false" ​ ​ ​ ​tabindex="0" ​ ​
​ ​
aria-labelledby="chk1-label"></span> ​
​ ​
<span​ ​id="chk1-label">Remember ​ ​ my preferences</span> ​
 
See how the id ​ ​ of the label and the aria-labelledby ​ ​ attribute have the same value? 
 
You could also add the aria-labelledby
​ ​ attribute to label a modal dialog: 
 
​ ​
<div​ ​role="dialog" ​ ​aria-labelledby="signup-title"> ​ ​ ​
​ ​
<h2​ ​id="signup-title">Sign ​ ​ ​
up</h2>

<button>Close</button> ​
​ ​ ​
<label for="email-1">Email:</label> ​ ​ ​
<input​ ​type="email" ​ ​ ​ ​name="Email" ​ ​ ​ ​
​ ​id="email-1" ​ ​/>
​ ​
<label​ ​for="password-1">Password:</label> ​ ​ ​
<input​ ​type="password"​ ​ ​ ​
​ ​name="Password" ​ ​
​ ​id="password-1" ​ ​/>
<input​ ​type="submit" ​ ​ ​ ​value="Sign ​ ​ up!"​ ​/>
</div>
 

50 
~ llh\.!,/lindsey
1Jl
wit

Sources:  
● MDN - Using the aria-labelledby attribute bit.ly/mdn-aria-labelledby
​  
● W3C - aria-labelledby bit.ly/w3c-aria-labelledby
​  

aria-describedby 
The ​aria-describedby​ attribute identifies the element that ​describes​ the object. This attribute's 
value takes on a string that matches the id ​ ​ of the element used to describe it. What's important to 
note here is that it announces after ​ ​ the element that it controls. The text value of the description 
element tends to be more verbose. It could be something like giving more instructions or details 
for formatting. 
 
​ ​
<label​ ​for="name-field">Name:</label> ​ ​ ​
​ ​
<input​ ​id="name-field" ​ ​
​ ​type="text" ​ ​
​ ​aria-describedby="formatting" ​ ​/>
​ ​
<em​ ​id="formatting">Format ​ ​ your name "Last Name, First Name Middle
Initial</em>​
 
See how the id ​ ​ of the label and the aria-describedby
​ ​ attribute have the same value?  
 
Using ​aria-describedby​ is part of the tooltip pattern. It looks similar to the above example, but 
we have a role="tooltip"
​ ​ on the id's element. I prefer to use tooltips minimally and only as 
quick hints. According to Sarah Higley, the best practices for tooltips are as follows: 
● Only interactive elements should trigger tooltips. 
● Tooltips should directly describe the UI control that triggers them (i.e., do not create a 
control purely to trigger a tooltip). 
● Use aria-describedby or aria-labelledby to associate the UI control with the tooltip. Avoid 
aria-haspopup and aria-live. 
● Do not use the title attribute to create a tooltip. 
● Do not put essential information in tooltips. 
● Provide a means to dismiss the tooltip with both keyboard and pointer. 
● Allow the mouse to easily move over the tooltip without dismissing it. 
● Do not use a timeout to hide the tooltip.17 
 
​ ​
<label​ ​for="email-field">Email:</label> ​ ​ ​
<input​ ​type="email" ​ ​ ​ ​ ​
​ id="email-field" ​ ​
​ aria-describedby="desc-email"
​ ​ />

​ ​
<span​ ​role="tooltip" ​ ​
​ ​id="desc-email">Be ​ ​ sure to format your email it
properly!</span> ​
 
Source: W3C - aria-describedby bit.ly/w3c-aria-describedby
​  

17
"Tooltips in the time of WCAG 2.1 | Sarah Higley." 17 Aug. 2019, 
https://sarahmhigley.com/writing/tooltips-in-wcag-21/.​ Accessed 21 Oct. 2020. 

51 
~ llh\.!,/lindsey
1Jl
wit

aria-label 
We briefly mentioned aria-label
​ ​ in Chapter 2, when we were talking about Form Labels. This 
attribute is my least favorite of the frequently used attributes. I was tempted not to mention it. 
However, because you'll likely see this out in the wild, it deserves to be explained. The 
aria-label​ reads whatever you put as the value. 
 
For example, I saw the following code on the Twitter login screen. I stripped out the HTML 
children and most of the attributes for readability. In this example, VoiceOver reads "Footer, 
Navigation." 
 
​ ​
<nav​ ​aria-label="Footer"> ​
<!-- children elements -->
</nav>
 
Source: W3C - aria-label bit.ly/w3c-aria-label
​  

aria-expanded 
The ​aria-expanded​ attribute indicates whether the element or a grouping element it controls is 
currently expanded or collapsed. This attribute is helpful for buttons that present new information 
when pressed. It takes on true or false values, and JavaScript usually toggles the value when the 
user presses a button. 
 
​ ​
<button​ ​aria-expanded="false">Helpful ​ ​ ​
Links</button>
<ul>
<li>
​ ​
<a ​href="https://www.a11yproject.com/">The ​ ​ A11Y Project</a>​
</li>
<li>
​ ​
<a ​ ​href="https://www.w3.org/WAI/WCAG21/quickref/">How ​ ​ to Meet WCAG (Quick
Reference)</a>​
</li>
</ul>
 
The button will read "Helpful Links, collapsed, button." Ideally, we should set the unordered list to 
display: none;​ when the button is collapsed. We don't want the user to have access to those 
links unless the button is expanded. 
 
​ ​
<button​ ​aria-expanded="true">Helpful
​ ​ ​
Links</button>
<ul>
<li>

52 
~ llh\.!,/lindsey
1Jl
wit

​ ​
<a ​href="https://www.a11yproject.com/">The ​ ​ A11Y Project</a> ​
</li>
<li>
​ ​ ​
<a ​ ​href="https://www.w3.org/WAI/WCAG21/quickref/"> ​ ​
​How to Meet WCAG (Quick Reference)
</a>
</li>
</ul>
 
The button will read "Helpful Links, expanded, button." The unordered list should be set to 
display: block;​ when the button is expanded. 
 
I like to use the aria-expanded
​ ​ values to style instead of using a class. I created an example on 
CodePen so you could see: bit.ly/codepen-aria-expanded
​  
 
Source: W3C - aria-expanded bit.ly/w3c-aria-expanded
​  

aria-haspopup 
The ​aria-haspopup​ attribute indicates that the element has a popup context menu or sub-level 
menu. This attribute is different from aria-expanded
​ ​ as it doesn't toggle state; it's a property. 
However, we usually add this attribute to an element that has aria-expanded.
​ ​ It usually takes on 
the values of true or false, but it can also take the values: menu, listbox, tree, grid, or dialog. 
 
​ ​
<button​ ​aria-expanded="false" ​ ​
aria-haspopup="true">Helpful ​ ​ ​
Links</button>
<ul>
<li>
​ ​
<a ​href="https://www.a11yproject.com/">The ​ ​ ​
A11Y Project</a>
</li>
<li>
​ ​ ​
<a ​ ​href="https://www.w3.org/WAI/WCAG21/quickref/"> ​ ​
​How to Meet WCAG (Quick Reference)
</a>
</li>
</ul>
 
This will read "Helpful Links, menu popup collapsed, button" 
 
Source: W3C - aria-haspopup bit.ly/w3c-aria-haspopup
​  

53 
~ llh\.!,/lindsey
1Jl
wit

aria-hidden 
The ​aria-hidden​ attribute removes an element from the accessibility tree. When we do this, it 
won't be available for assistive technology. It's helpful when there are necessary elements for the 
design, but would be confusing for a screen reader user. It takes on the values of true or false. 
 
<nav>
<ol>
​ ​
<li><a ​ ​href="/">Home</a></li>
​ ​ ​
<li>
<span ​ ​
​aria-hidden="true">»</span>
​ ​ ​
​ ​
<a ​ ​href="/about">About</a>
​ ​ ​
</li>
</ol>
</nav>
 
This will read "Link, About" without the special character. Not all screen readers announce these 
characters. I always include it anyway because I'd rather not need it than accidentally be annoying. 
 
As we talked about in Chapter 2, aria-hidden
​ ​ is pretty handy when we want to hide SVGs or mark 
them as decorative. That's the other instance where I use aria-hidden
​ ​ the most. 
 
Source: MDN - Using the aria-hidden attribute bit.ly/mdn-aria-hidden
​  

aria-current 
Have you ever gone to a website, and there's a link with different styling than the others to 
indicate that you are on the current page? Most of the time, people use a CSS class. But that 
doesn't help assistive technology users understand what that means semantically. That's where the 
aria-current attribute comes in handy.  
 
However, this attribute isn't only handy for navigations. It also helps indicate the current time 
during a schedule or the current date in a calendar. I could see conferences using this attribute to 
highlight current sessions. This attribute takes on the following values: page, step, location, date, 
time, true, false. 
 
● page - indicate a link within a set of links is the currently-displayed page. 
● step - indicate a link within a step indicator for a step-based process. 
● location - indicate the current location within an environment or context. 
● date - indicate the current date within a calendar. 
● time - indicate the current time within a timetable. 
 

54 
~ llh\.!,/lindsey
1Jl
wit

<nav>
<ul>
<li><a ​ ​href="/"​ ​ ​ ​
​ ​aria-current="page">Home</a></li>
​ ​ ​
​ ​
<li><a ​ ​href="/about">About</a></li>
​ ​ ​
​ ​
<li><a ​ ​href="/contact">Contact</a></li>
​ ​ ​
</ul>
</nav>
 
When we focus on the home link, it will read "Current page, link, Home." 
 
Similarly to how I did with aria-expanded, you can also use aria-current to style an active link 
instead of a class. You can see how I did it on CodePen: bit.ly/codepen-aria-current
​  
 
Sources:  
● Tink - Using the aria-current attribute bit.ly/tink-aria-current
​  
● W3C - aria-current bit.ly/w3c-aria-current
​  

aria-live 
Note: This section is a high-level overview. We are going to go more in-depth with aria-live in the 
JavaScript section. 
 
Sometimes we want to announce a dynamic change without changing the focus of where we are. 
We want to let the user know that "Hey, this was saved." Or maybe we want to say, "Hey, you have 
an error on your form." That's where aria-live comes in handy. 
 
Whenever we update the innerText of the aria-live region, we get an announcement. It takes on 
the values of polite, assertive, or off. Polite will wait until the screen reader finishes announcing 
what it’s reading. Assertive will interrupt what the screen reader is reading. 
 
Note this works very similarly to the status role (aria-live=" polite") and the alert role (aria-live" 
assertive") 
 
A quick example of how that would work. We have a form and an aria-live region set to polite. 
Right now that region is empty. 
 
​ ​
<div​ ​aria-live="polite"></div> ​
<form>
​ ​
<label ​ ​for="email-field">Email:</label>
​ ​ ​
​ ​
<input ​ ​type="email" ​ ​
​ ​id="email-field" ​ ​/>
​ ​
<button ​ ​type="submit">Submit</button>
​ ​ ​
</form>
 

55 
~ llh\.!,/lindsey
1Jl
wit

In JavaScript, we will set the innerText of the aria-live region to "Your email has been submitted!" 
 
​ ​
<div​ ​aria-live="polite">Your ​ ​ ​
email has been submitted!</div>
<form>
​ ​
<label ​ ​for="email-field">Email:</label>
​ ​ ​
​ ​
<input ​ ​type="email" ​ ​
​ ​id="email-field" ​ ​/>
​ ​
<button ​ ​type="submit">Submit</button>
​ ​ ​
</form> 
 
After submitting, we have "Your email has been submitted!" inside the div. VoiceOver reads that 
message after we press submit, but focus STAYS on the submit. 
 
If you want to play around with how this works, I made a codepen: bit.ly/codepen-aria-live.
​ ​ We’ll 
go over the code a little bit more in the JavaScript and interactions section. 
 
Sources:  
● MDN - ARIA live regions bit.ly/mdn-aria-live
​  
● W3C - aria-live bit.ly/w3c-aria-live
​  

   

56 
~ llh\.!,/lindsey
wit1Jl

Part 3 - JavaScript and Interaction 

   

57 
~ llh\.!,/lindsey
1Jl
wit

Chapter 6 - Patterns 
Well, here comes my favorite part of this book: JavaScript and Interaction. I am a JavaScript 
developer. Many designers and HTML/CSS focused developers critique JavaScript developers for 
creating inaccessible applications. I am in this weird middle ground because I am a JavaScript 
developer, and it might seem like I would get defensive. But listen, this critique is valid. 
Unfortunately, I think it contributes to the myth that JavaScript makes things inaccessible. As of 
right now (2020), some patterns need
​ ​ JavaScript to be accessible. We’ll talk about that in this 
section.  
 
I need to get on my soapbox (again) and remind people about their ethical responsibility as 
developers to fight ableism. It’s natural to center our own experience and how we interact with 
web applications. But we need to take a step back and remember there are many ways that a user 
interacts with the web. It has to make sense on a screen reader, and we have to be able to use our 
keyboard to perform the same functions. In this chapter, I’ll go over many different patterns and 
hand-code some of them. The only one we won’t hand code is the combobox pattern, which I’ll talk 
about more in that section. 

6.1 Modals & Dialogs 


In terms of semantic HTML, we do actually have a dialog element (bit.ly/mdn-dialog).
​ ​ But the 
dialog API isn't 100% supported by browsers, either. So we use ARIA and JavaScript to create an 
interactive modal dialog. It’s one of those times like I mentioned in Chapter 5 were sometimes the 
browser support isn’t there yet.  
 
In most of these sections I start talking about the standards and why those exist. But because 
modals tend to be such a problematic pattern when implemented poorly, I wanted to present you 
with a scenario. Imagine you only use your keyboard to navigate through web sites. You are on a 
page for a required application for your job. You go to the sign-up button, and a modal dialog pops 
up. But... uh wait? Where are you? Your keyboard focus is somewhere behind the screen. So you 
press the tab key. You keep accessing buttons behind the modal. You start getting anxious. You 
need this application to do your job! What do you do? 
 
Modals and dialogs are very common in interactive applications. But rarely do people consider this 
exact scenario. Because again, ableism. So what are some concrete patterns that you can think of 
to make accessible modals? 
 
First, consider if this content needs to live within a modal at all.​ I've seen people who use modals 
to create mini web pages instead of creating a new page for that content. It's one thing to have a 
quick sign up form. It's another to have a complete tablist of many pages of information inside a 
modal (something I have unfortunately encountered and worked on). 
 

58 
~ llh\.!,/lindsey
1Jl
wit

If you decide that the content needs to be in a modal, below are a few considerations to make. 
 
Precise controls and labeling. ​We want to be sure we are adding the role to the wrapper of the 
modal dialog. We also should add the aria-modal
​ ​ attribute; however, the support is shaky as of 
this writing, but it doesn’t hurt to add.18 
 
We need to ensure that there's a clear purpose for interacting with the modal and what to do 
inside the modal. Is the modal a form? Is the modal asking us if we are sure we want to take 
action? Is it clear how to interact with the modal? We also want to make sure we have a button to 
close the modal. 
 
Properly shifting focus to the modal on open. We ​ want to take the focus away from the trigger that 
opened the modal. Then we want to shift that focus to the first focusable element. There should 
always be at least one focusable element in every modal, even if it's only the close button. 
Additionally, I've seen people set the dialog wrapper to have a tabindex of -1 and shift the focus to 
the wrapper upon open. This strategy technically
​ ​ works, but it's much clearer to focus on a smaller 
and interactive element.  
 
Focus Trapping.​ Once we shift focus inside the modal, we have to trap the focus in the modal. 
What is focus trapping? It's taking the focus and keeping it in a specific region. When you get to the 
end of that region, it goes back up to the region's top. This technique ensures that the focus stays 
in that modal unless you exit. The user won't be using their keyboard to tab somewhere they can't 
see. 
 
Additionally, I like to make it visually clear with an "overlay" that you can't access behind the 
modal. Usually, I do this with a transparent black background behind the modal. 
 
Upon close, go back to where you started when you opened the modal. We ​ want to allow the user 
to continue navigating the web page from where they left off before the modal opened. Therefore, 
we should move the focus back to where it was before. Most times, this will be the trigger to open 
the modal. 
 
Should close on ESC key.​ In addition to having a clear close button, we should also be listening for 
the user to press the Escape key. That should close the modal. 
 
Source: W3C - Modal Dialog Example bit.ly/w3c-example-dialog
​  
 

18
"The current state of modal dialog accessibility | TPG – The ...." 29 Jun. 2018,
https://developer.paciellogroup.com/blog/2018/06/the-current-state-of-modal-dialog-accessibility/.​
Accessed 1 Nov. 2020.

59 
~ llh\.!,/lindsey
1Jl
wit

Should I hand-code this? Confession,


​ the first time I hand-coded a Modal myself was while writing 
this book. Why? The logic can get complicated quickly. A lot of times, the libraries available have 
the necessary accessibility requirements.  
 
The pro to hand-coding a modal is to improve your JavaScript skills and get a deeper 
understanding of how the modal works. The cons are that it can be brittle, you need to write 
additional tests, and they require a lot of work. 
 
I'll walk through all the steps I took, but if you want to skip this and use a library, that's ok. If you 
use a library, be sure to test it with all the criteria, even if the library has an "accessible modal" in 
the name.  
 
I'll be going into concepts like creating a prototype, which can get advanced. I won't be going into 
depth explaining these concepts, but I'll be sure to post sources and footnotes. We'll be creating a 
Dialog constructor and then modifying the prototype with custom methods. 

Hand Coding a Modal 


 
The way I structure my code is by creating 3 layers with id attributes: 
● The root (i.e., the page) 
● The modal dialog 
● The overlay 
 
​ ​
<div​ ​class="wrapper"> ​
​ ​
<div ​ ​id="root"></div> ​
​ ​
<div ​ ​id="overlay"></div> ​
​ ​
<div ​ ​id="dialog"></div> ​
</div>
 
Then we want to markup the dialog with all the appropriate ARIA attributes per the W3C-WAI spec
19
. We also want to add the modal content (which will be an email sign up form). 
 
<div
​ ​
​id="dialog"
​ ​
​role="dialog"
​aria-modal="true" ​ ​
​aria-labelledby="sign-up"​ ​
​tabindex="-1" ​ ​
>

19
"Modal Dialog Example | WAI-ARIA Authoring Practices 1.1." 
https://www.w3.org/TR/wai-aria-practices-1.1/examples/dialog-modal/dialog.html.​ Accessed 1 Nov. 2020. 

60 
~ llh\.!,/lindsey
1Jl
wit

​ ​
<h2​ ​id="sign-up">Sign ​ ​ Up</h2>​
<button​ ​id="close" ​ ​ ​ ​
​ ​class="close">Close</button> ​ ​ ​
<div>
​ ​ ​
<label for="email-1">Email:</label> ​ ​ ​
<input type="email" ​ ​ ​ ​ ​name="Email" ​ ​ ​ ​
​ ​id="email-1" ​ ​/>
</div>
<div>
​ ​
<label​ ​for="password-1">Password:</label> ​ ​ ​
<input​ ​type="password" ​ ​ ​ ​
​ ​name="Password" ​ ​
​ ​id="password-1" ​ />
</div>
<input​ ​type="submit" ​ ​ ​ ​value="Sign ​ ​ up!"​ ​/>
</div>
 
Now that we have the markup, let's make the Dialog constructor. First, we will pass in the dialogEl, 
the overlayEl, and the rootEl. 
 
function​ Dialog(dialogEl,
​ ​ ​ ​ overlayEl,
​ ​ rootEl)
​ ​ {
​ ​
​this.dialogEl ​ = dialogEl;
​ ​
​this.overlayEl ​ = overlayEl;
​ ​
​this.rootEl ​ = rootEl;
}
 
Then we will grab all the focusable elements within the dialogEl. We will also get the first and last 
focusable items. The first and last focusable items will be necessary for focus trapping later. 
 
function​ Dialog(dialogEl,
​ ​ ​ ​ overlayEl,
​ ​ ​ rootEl) ​ {
​ ​
​this.dialogEl ​ = dialogEl;
​ ​
​this.overlayEl ​ = overlayEl;
​ ​
​this.rootEl ​ = rootEl;

// get the focusable nodelist


​ ​
​this.focusableEls ​ ​
​ = this.dialogEl.querySelectorAll(
​ ​ ​ ​
​'a[href], area[href], input:not([disabled]), select:not([disabled]),
​textarea:not([disabled]), button:not([disabled]), [tabindex="0"]'
);

​// grab the first focusable element


​ ​
​this.firstFocusable ​ ​
​ = this.focusableEls[0];
​ ​ ​ ​

// grab the last focusable element


​ ​
​this.lastFocusable ​ ​ ​ ​ ​ ​ ​
​ = this.focusableEls[this.focusableEls.length
​ ​ ​ - 1];
​ ​
}

61 
~ llh\.!,/lindsey
1Jl
wit

 
Before we modify the prototype, we want to create a new Dialog using this constructor. We can 
pass in all the elements we made in the HTML. At the end of our JavaScript file, we want to create 
a new Dialog with our elements. 
 
const​ dialog
​ ​ = document.getElementById("dialog");
​ ​ ​ ​
const​ overlay
​ ​ = document.getElementById("overlay");
​ ​ ​ ​
const​ root ​ ​ = document.getElementById("root");
​ ​ ​ ​

const​ myDialog
​ ​ = new​ ​ Dialog(dialog,
​ ​ overlay, root);
 
If we console log our variable myDialog, we will be able to see all the properties we are adding 
and all the methods we'll be adding to the prototype. If you're newer to this way of thinking, it can 
help you observe what's happening beneath the hood. 
 
We want to modify the Dialog prototype by adding a few methods. First, let's add the open method 
and store the element that triggers the open so that we can eventually return to it when we close 
the modal. 
 
​ ​
Dialog.prototype.open ​ ​ ​ = function
​ ​ () {
​const​ ​Dialog​ = this;
​ ​

​ ​
​this.focusedElBeforeOpen ​ = document.activeElement;
​ ​
};
 
What else happens upon open?  
● We enable focus trapping. 
● We start listening for the escape key to close the modal. 
● We remove the hidden attributes from the overlay and the modal dialog (I will be adding 
that into the constructor a bit later) 
● We set the root to be aria-hidden 
● We focus on the first focusable element that we got in the constructor 
● Close the modal if we click outside of the modal (or, in this case, on the overlay). 
 
Let's add the basic functionality to this open method before we start adding even more methods. 
 
​ ​
Dialog.prototype.open ​ ​ ​ = function
​ ​ () {
​const​ ​Dialog​ = this;
​ ​

​ ​
​this.dialogEl.removeAttribute("hidden");
​ ​ ​ ​ ​
​ ​
​this.overlayEl.removeAttribute("hidden");
​ ​ ​ ​ ​
​ ​
​this.rootEl.setAttribute("aria-hidden",
​ ​ ​ ​ ​
​ true);

62 
~ llh\.!,/lindsey
1Jl
wit

​ ​
​this.focusedElBeforeOpen ​ = document.activeElement;
​ ​

​// focuses on the first focusable element in the modal


​requestAnimationFrame(() ​ => {
​ ​
​this.firstFocusable.focus();
​ ​ ​
});
}; 
 
Note: I added the requestAnimationFrame to resolve a bug in safari. this.firstFocusable was 
undefined unless I gave it a little bit of time to load. I found this solution on Scott O'Hara's 
accessible modal library bit.ly/vanilla-js-request-animation-fix.
​ ​ 
 
To help control where we can click when the modal is open, I added some CSS. Specifically, 
z-index and positioning to the root, dialog, and overlay: 


#root ​ {
​z-index:
​ 1;
​ ​
}


.overlay ​ {
​position: ​ absolute;
​ ​
​width:
​ 100%; ​ ​
​height: ​ 100%; ​ ​
​top:
​ 0;
​ ​
​background: ​ rgba(0,
​ ​ ​ ​ 0,
​ ​ 0,
​ ​ 0.5);
​ ​
​z-index: ​ 2; ​ ​
}


.dialog ​ {
​position: ​ absolute;
​ ​
text-align ​: center; ​ ​
​top:
​ 10vh;
​ ​
​left:​ 10vw;​ ​
​width: ​ 80vw;​ ​
​height: ​ 80vh;​ ​
​background: ​ white;
​ ​
​padding: ​ 1rem;​ ​
z-index ​: 3; ​ ​
}
 
Next, let's create the close method. 

63 
~ llh\.!,/lindsey
1Jl
wit

 
​ ​
Dialog.prototype.close ​ ​ ​ = function
​ ​ () {
​ ​
​this.dialogEl.setAttribute("hidden",
​ ​ ​ ​ ​ "");
​ ​
​ ​
​this.overlayEl.setAttribute("hidden",
​ ​ ​ ​ ​ "");
​ ​
​ ​
​this.rootEl.removeAttribute("aria-hidden");
​ ​ ​ ​ ​

​if​ (this.focusedElBeforeOpen)
​ ​ ​ ​ {
​ ​
​this.focusedElBeforeOpen.focus();
​ ​ ​
}
};
 
Then we go back to the open() method and add that event listener to the overlay: 
 
​ ​
Dialog.prototype.open​ ​ ​ = function
​ ​ () {
​const​ ​Dialog​ = this;
​ ​

​ ​
​this.dialogEl.removeAttribute("hidden");
​ ​ ​ ​ ​
​ ​
​this.overlayEl.removeAttribute("hidden");
​ ​ ​ ​ ​
​ ​
​this.rootEl.setAttribute("aria-hidden",
​ ​ ​ ​ ​
​ true);

​ ​
​this.focusedElBeforeOpen ​ = document.activeElement;
​ ​

​ ​
​this.closeOnOverlayClick ​ = function
​ ​ () {
​Dialog.close();
​ ​ ​
};

​ ​
​this.overlayEl.addEventListener("click",
​ ​ ​ ​ ​ ​
​ this.closeOnOverlayClick);
​ ​

// focuses on the first focusable element in the modal


​requestAnimationFrame(()
​ => {
​ ​
​this.firstFocusable.focus();
​ ​ ​
});
};
 
Now we want to create the functionality for focus trapping and closing on the escape key. We'll 
contain this in a keydown event that calls a function that calls a method (whew). 
 
Let's create the method first and grab those keys. 
 
​ ​ ​ ​
Dialog.prototype.addTrappingAndEscape ​ = function
​ ​ (e)
​ ​ {
​const​ ​Dialog​ = this;
​ ​

64 
~ llh\.!,/lindsey
1Jl
wit

​const​ ​tabKey​ = "Tab";


​ ​
​const​ ​escKey​ = "Escape";
​ ​
};
 
 
Now let's think through the logic for focus trapping. We want the default tabbing behavior except 
in two cases. Those cases are when we are on the first and last focusable element inside the 
modal. If we are doing a backward tab (meaning pressing Tab & the Shift key) on the first element, 
we want to go to the last element. If we are doing a forward tab (only Tab key) on the last element, 
we want to go to the first element. We'll need to create functions that prevent the default behavior
20
for two specific cases. The first case is when we are going backward on the first focusable 
element. The second case is when we are going forward while focusing on the last focusable 
element. When we prevent the default behavior, we need to tell it what it's doing instead. So here 
we are adding handleBackwardTab and handleForwardTab 
 
​ ​ ​ ​
Dialog.prototype.addTrappingAndEscape ​ = function
​ ​ (e)
​ ​ {
​const​ ​Dialog​ = this;
​ ​

​const​ ​tabKey​ = "Tab";


​ ​
​const​ ​escKey​ = "Escape";
​ ​

​function​ ​handleBackwardTab() ​ {
​if​ (document.activeElement
​ ​ === Dialog.firstFocusable)
​ ​ ​ ​ {
​e.preventDefault();
​ ​ ​
​Dialog.lastFocusable.focus();
​ ​ ​ ​ ​
}
}

​function​ ​handleForwardTab()
​ {
​if​ (document.activeElement
​ ​ === Dialog.lastFocusable)
​ ​ ​ ​ {
​e.preventDefault();
​ ​ ​
​Dialog.firstFocusable.focus();
​ ​ ​ ​ ​
}
}
};
 

20
"Event.preventDefault() - Web APIs | MDN." 29 Mar. 2020, 
https://developer.mozilla.org/en/docs/Web/API/Event/preventDefault.​ Accessed 2 Nov. 2020. 

65 
~ llh\.!,/lindsey
1Jl
wit
21
Then we want to create a switch statement for the key. We'll write some logic about which 
function is being called when.  
 
Here's some logic we want to check for with the Tab key case: 
● If there's only one focusable element 
● If we are also using the shift key (meaning we're going backward) 
 
Inside addTrappingAndEscape, let's start the switch and use those variables we created for the 
cases: 
 
switch​ (e.key) ​ ​ ​ ​ {
​case​ ​tabKey: ​
​break; ​
​case​ ​escKey: ​
​break; ​
​default: ​
​break; ​
}
 
Because the escape key is simpler, let's add that logic in first: 
 
switch​ (e.key) ​ ​ ​ ​ {
​case​ ​tabKey: ​
​break; ​
​case​ ​escKey: ​
​Dialog.close(); ​ ​ ​
​break; ​
​default: ​
​break; ​
}
 
If there is only one focusable element, we will need to prevent the default behavior on the Tab Key 
as it'll stay focused on that one element. Let's add the logic inside the tabKey case for if there's 
only one focusable element: 
 
switch​ (e.key) ​ ​ ​ ​ {
​case​ ​tabKey: ​
​if​ (Dialog.focusableEls.length
​ ​ ​ ​ ​ ​ === 1)
​ ​ {
​e.preventDefault();
​ ​ ​

21
"switch - JavaScript - MDN Web Docs - Mozilla." 25 Oct. 2020, 
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/switch.​ Accessed 1 Nov. 
2020. 

66 
~ llh\.!,/lindsey
1Jl
wit

}
​break;​
​case​ ​escKey: ​
​Dialog.close();
​ ​ ​
​break; ​
​default: ​
​break; ​
}
 
Now let's add in the conditional logic if we have the shift key selected or not: 
 
switch​ (e.key) ​ ​ ​ ​ {
​case​ ​tabKey: ​
​if​ (Dialog.focusableEls.length
​ ​ ​ ​ ​ ​ === 1)
​ ​ {
​e.preventDefault();
​ ​ ​
}
​if​ (e.shiftKey)
​ ​ ​ ​ {
​handleBackwardTab(); ​
} ​else​ {
​handleForwardTab(); ​
}
​break; ​
​case​ ​escKey: ​
​Dialog.close(); ​ ​ ​
​break; ​
​default: ​
​break; ​
}
 
Now let's add this method to the open prototype, and add the keydown event listener on the 
dialog. 
 
​ ​
Dialog.prototype.open ​ ​ ​ = function
​ ​ () {
​const​ ​Dialog​ = this; ​ ​

​ ​
​this.dialogEl.removeAttribute("hidden");
​ ​ ​ ​ ​
​ ​
​this.overlayEl.removeAttribute("hidden");
​ ​ ​ ​ ​
​ ​
​this.rootEl.setAttribute("aria-hidden",
​ ​ ​ ​ ​
​ true);

​ ​
​this.focusedElBeforeOpen ​ = document.activeElement;
​ ​

​ ​
​this.handleKeyDown ​ = function
​ ​ (e)
​ ​ {

67 
~ llh\.!,/lindsey
1Jl
wit

​Dialog.addTrappingAndEscape(e);
​ ​ ​ ​ ​
};

​ ​
​this.closeOnOverlayClick ​ = function
​ ​ () {
​Dialog.close();
​ ​ ​
};

​ ​
​this.dialogEl.addEventListener("keydown",
​ ​ ​ ​ ​ ​
​ this.handleKeyDown);
​ ​

​ ​
​this.overlayEl.addEventListener("click",
​ ​ ​ ​ ​ ​
​ this.closeOnOverlayClick);
​ ​

// focuses on the first focusable element in the modal


​requestAnimationFrame(()
​ => {
​ ​
​this.firstFocusable.focus();
​ ​ ​
});
};
 
We're almost done! For performance reasons, on close, we should remove the event listeners upon 
closing: 
 
​ ​
Dialog.prototype.close ​ ​ ​ = function
​ ​ () {
​ ​
​this.dialogEl.setAttribute("hidden",
​ ​ ​ ​ ​ "");
​ ​
​ ​
​this.overlayEl.setAttribute("hidden",
​ ​ ​ ​ ​ "");
​ ​
​ ​
​this.rootEl.removeAttribute("aria-hidden");
​ ​ ​ ​ ​

​ ​
​this.dialogEl.removeEventListener("keydown",
​ ​ ​ ​ ​ ​
​ this.handleKeyDown);
​ ​
​ ​
​this.overlayEl.removeEventListener("click",
​ ​ ​ ​ ​ ​
​ this.closeOnOverlayClick);
​ ​

​if​ (this.focusedElBeforeOpen)
​ ​ ​ ​ {
​ ​
​this.focusedElBeforeOpen.focus();
​ ​ ​
}
};
 
Then, we want to add the hidden attribute to the dialog and overlay upon load in the constructor. I 
did this later because I didn’t want to hide it until I knew it was working.  
 
function​ Dialog(dialogEl,
​ ​ ​ ​ overlayEl,
​ ​ ​ rootEl)​ {
​ ​
​this.dialogEl ​ = dialogEl;
​ ​
​this.overlayEl ​ = overlayEl;
​ ​
​this.rootEl ​ = rootEl;

​ ​
​this.focusableEls ​ ​
​ = this.dialogEl.querySelectorAll(
​ ​ ​ ​

68 
~ llh\.!,/lindsey
1Jl
wit

​'a[href], area[href], input:not([disabled]), select:not([disabled]),


​textarea:not([disabled]), button:not([disabled]), [tabindex="0"]'
);

​ ​
​this.firstFocusable ​ ​
​ = this.focusableEls[0];
​ ​ ​ ​

​ ​
​this.lastFocusable ​ ​ ​ ​ ​ ​ ​
​ = this.focusableEls[this.focusableEls.length
​ ​ ​ - ​1];

​ ​
​this.dialogEl.setAttribute("hidden",
​ ​ ​ ​ ​ "");
​ ​
​ ​
​this.overlayEl.setAttribute("hidden",
​ ​ ​ ​ ​ "");
​ ​
}
 
We also want to add event listeners to open and close buttons that we provide. 
 
​ ​ ​
Dialog.prototype.addEventListeners ​
= function ​ (openSelector, closeSelector) {
​const​ ​Dialog​ = this;
​ ​
​const​ ​openEls​ = document.querySelectorAll(openSelector);
​ ​ ​ ​
​const​ ​closeEls​ = document.querySelectorAll(closeSelector);
​ ​ ​ ​

​openEls.forEach(function
​ ​ ​ ​ ​ (el)
​ ​ {
​el.addEventListener("click",
​ ​ ​ ​ ​ function
​ ​ () {
​Dialog.open();
​ ​ ​
});
});

​closeEls.forEach(function
​ ​ ​ ​ ​ (el)
​ ​ {
​el.addEventListener("click",
​ ​ ​ ​
function ​ () {
​Dialog.close();
​ ​ ​
});
});
};
 
Then after we create the new Dialog, let's pass those selectors into the event listeners. 
 
const​ myDialog
​ ​ = new
​ ​ Dialog(dialog,
​ ​ overlay, root);
​ ​
myDialog.addEventListeners(".open", ​ ​ ​ ".close");
​ ​

And there you have it! Would I usually go through all this trouble? Probably not. As I said before, 
there are more libraries out there that do this same thing. But I think sometimes it's helpful to do 
so you can learn more about JavaScript.  

69 
~ llh\.!,/lindsey
1Jl
wit

 
Final code: bit.ly/codepen-dialog
​  

Helpful Libraries 
If you don't want to write a modal by hand, there are plenty of libraries that you can use. 
● Vanilla JS: Accessible Modal Dialog by Scott O'Hara bit.ly/github-a11y-modal
​  
● ReactJS: React Modal by ReactJS bit.ly/github-react-modal
​  
● VueJS: Vue Accessible Modal by Andrew Vasilchuk bit.ly/github-vue-modal
​  
 
Sources:  
● Creating An Accessible Modal Dialog bit.ly/a11y-modal-dialog-tutorial
​  
● The Paciello Group - The current state of modal dialog accessibility 
bit.ly/paciello-dialog-state 

Automatic Pop-Ups - A Rant 


There's not much I could find on the internet about the accessibility of "automatic pop-ups." When 
I say automatic pop-ups, I mean those modals you get after a certain amount of time scrolling on a 
website. Usually, it prompts you with a newsletter sign-up or a coupon code for an e-commerce 
site. Automatic pop-ups are common on blogging websites for folks who want to build their email 
list. Which fair. I have an email list. A lot of my readers know about my book through my email list. 
I don't think there's any shame in building an email list. But I think automatic pop-ups are a 
terrible idea. There are ways to alert users to your email list without interrupting their reading. 
 
In my professional opinion, sending focus anywhere without the user doing something to trigger it 
is a dark pattern. Even if you were using all the standards I've gone over in this section. Focus 
trapping your users without their consent to pressure them to sign up for your email list is 
unethical. Also, interrupting reading is jarring for cognitively disabled people like myself. 
 
I've listened to so many business podcasts that say, "I know people hate them, but they are 
effective." "Effective" may mean manipulative, but that's beyond the point. I don't believe you can 
give a disabled user a positive experience using this tactic. Many argue it's a terrible experience all 
around. I would rather market my product to my target audience, not to a subset of my audience. 
I'm going to be bold and say that this dark pattern is a form of discrimination. 
 
Rant over. Onto the Accordion pattern. 

6.2 Accordions 
I talked about in Chapter 5 how I don’t use the Details element because of browser support. Here 
I’ll show you the conventions used to create an accessible accordion with ARIA and semantic 
HTML.  
 

70 
~ llh\.!,/lindsey
1Jl
wit

The plain language pattern of the accordion is this (we’ll get more technical right after this) 
● The accordion “headers” are semantic buttons so we can use the Tab key to access them 
● We cannot access any content in closed accordion panels 
● We should use attributes like aria-expanded ​ ​ to communicate the state of the accordion 
panels. 
● When we are on the accordion headers, we should be able to use the up and down arrows 
to shift focus to the sibling headers. 
● If the Home and End key are available, the Home key should shift focus to the first 
accordion header and the End key should shift focus on the last accordion header. 
 
Some notes about the starting markup that we haven’t already covered. 
● Panels have the role="region"​
● Panel has aria-labelledby
​ ​ attribute to associate it with the button 
● Button has aria-controls*​ ​ with the id of the panel 
*Note on aria-controls
​ ​ - The support on this attribute isn’t consistent. I still like to use it 
because it makes my JavaScript logic easier.22  
 
​ ​
<div​ ​id="app"> ​
<h1> ​Accordion</h1> ​
​ ​
<div ​ ​id="accordionGroup" ​ ​
​ ​class="accordion"> ​
<button
​ ​
​id="accordion-button-1"
​ ​
​class="accordion__button"
​ ​
​aria-controls="accordion-section-1"
>
Section 1
​ </button>
<div
​ ​
​role="region"
​ ​
​id="accordion-section-1"
​ ​
​aria-labelledby="accordion-button-1"
​ ​
​class="accordion__section"
​ >
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer
vitae lectus in sapien tempus maximus.
​ </p>
</div>
<button
​ ​
​id="accordion-button-2"

22
"Using the aria-controls attribute - Tink - Léonie Watson." 21 Nov. 2014, 
https://tink.uk/using-the-aria-controls-attribute/.​ Accessed 1 Nov. 2020. 

71 
~ llh\.!,/lindsey
1Jl
wit

​ ​
​class="accordion__button"
​ ​
​aria-controls="accordion-section-2"
​ >
Section 2
​ </button>
<div
​ ​
​role="region"
​ ​
​id="accordion-section-2"
​ ​
​aria-labelledby="accordion-button-2"
​ ​
​class="accordion__section"
​>
<p>
Etiam laoreet hendrerit est non vulputate. Quisque lacinia justo leo,
at pretium nulla dapibus nec.
</p>
</div>
<button
​ ​
​id="accordion-button-3"
​ ​
​class="accordion__button"
​ ​
​aria-controls="accordion-section-3"
​ >
Section 3
​</button>
<div
​ ​
​role="region"
​ ​
​id="accordion-section-3"
​ ​
​aria-labelledby="accordion-button-3"
​ ​
​class="accordion__section"
​ >
<p>
Mauris in porta lacus, eget imperdiet nibh. Sed porttitor bibendum
ornare.
​ </p>
</div>
</div>
</div>
 
Make sure that we have the hidden attribute on the panels and aria-expanded* 
 
*Note: I am going to address this example in the Progressive Enhancement chapter. I am 
hard-coding the initial states for aria-expanded
​ ​ and hidden
​ ​ for the sake of learning the 

72 
~ llh\.!,/lindsey
1Jl
wit

accordion patterns. Normally, I wouldn’t hard-code this into the HTML. Please be sure to see how I 
would approach this in Chapter 7. 
 
​ ​
<div​ ​id="app"> ​
<h1> ​Accordion</h1> ​
​ ​
<div ​ ​id="accordionGroup" ​ ​
​ ​class="accordion">

<button
​ ​
​id="accordion-button-1"
​ ​
​class="accordion__button"
aria-expanded="false" ​
​ ​
​aria-controls="accordion-section-1"
>
Section 1
​ </button>
<div
​ ​
​role="region"
​ ​
​id="accordion-section-1"
​ ​
​aria-labelledby="accordion-button-1"
​ ​
​class="accordion__section"
hidden
​ >
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer
vitae lectus in sapien tempus maximus.
​ </p>
</div>
<button
​ ​
​id="accordion-button-2"
​ ​
​class="accordion__button"
aria-expanded="false" ​
​ ​
​aria-controls="accordion-section-2"
​ >
Section 2
​ </button>
<div
​ ​
​role="region"
​ ​
​id="accordion-section-2"
​ ​
​aria-labelledby="accordion-button-2"
​ ​
​class="accordion__section"
hidden
​>
<p>

73 
~ llh\.!,/lindsey
1Jl
wit

Etiam laoreet hendrerit est non vulputate. Quisque lacinia justo leo,
at pretium nulla dapibus nec.
</p>
</div>
<button
​ ​
​id="accordion-button-3"
​ ​
​class="accordion__button"

aria-expanded="false"
​ ​
​aria-controls="accordion-section-3"
​ >
Section 3
​</button>
<div
​ ​
​role="region"
​ ​
​id="accordion-section-3"
​ ​
​aria-labelledby="accordion-button-3"
​ ​
​class="accordion__section"
hidden
​ >
<p>
Mauris in porta lacus, eget imperdiet nibh. Sed porttitor bibendum
ornare.
​ </p>
</div>
</div>
</div> 
 
The ​aria-expanded​ attribute helps communicate if the panel is expanded or collapsed. The 
hidden attribute hides the content so that we aren’t announcing content that isn’t expanded. 
 
Before I go any further, I’d like to add width to the wrapper and style those buttons. As of right 
now, they have no styling and look a bit ugly: 
 

.accordion ​ {
​width: ​ 35rem;
​ ​
}


.accordion__button ​ {
​position: ​ relative;
​ ​
​display: ​ block;
​ ​
​margin:
​ -1px​ ​
0 0;
​border:​ 1px ​ ​
solid #6505cc;

74 
~ llh\.!,/lindsey
1Jl
wit

​padding:​ 0.5rem
​ 1rem;​
​width:
​ 100%;
​ ​
​text-align: ​ left;​ ​
​color:​ #6505cc;
​ ​
​font-size: ​ 1rem; ​ ​
​background: ​ #eedbff;
​ ​
}
 
Also, let’s invert the background and text color for focus and hover styling: 
 

.accordion__button:focus, ​ ​ ​

.accordion__button:hover ​ ​ ​ {
​background: ​ #310363;
​ ​
​color:
​ #eedbff;
​ ​
}
 
I also like to add a CSS Triangle using an ::after ​ ​ pseudo-element. I also create some hover and 
focus styles on those as well. I found the CSS on CSS tricks.23 
 

.accordion__button::after ​ ​ ​ {
​content: ​ ""; ​ ​
​position: ​ absolute;
​ ​
​right:​ 1rem; ​ ​
​top:
​ 0.65rem;
​ ​
​width: ​ 0; ​ ​
​height: ​ 0; ​ ​
​border-left: ​ 10px ​ solid transparent;​
​border-right: ​ 10px ​ solid transparent;​
​border-top: ​ 15px ​ solid #6505cc; ​
}

​ ​ ​
.accordion__button:focus::after,​ ​ ​
​ ​ ​
.accordion__button:hover::after ​ ​ ​ {
​border-top-color:​ #eedbff;
​ ​
}
 
Now let’s get to writing JavaScript. First, we need to create an event listener that toggles the 
accordion open and closed. To do that, we need to loop through all the accordion buttons. Then we 
add an event listener to toggle the aria-expanded
​ ​ attribute to be true or false. We also need to 
remove the ​hidden​ attribute if the accordion is open. 

23
"CSS Triangle | CSS-Tricks." 29 Sep. 2016, https://css-tricks.com/snippets/css/css-triangle/.
​ ​ Accessed 31 
Oct. 2020. 

75 
~ llh\.!,/lindsey
1Jl
wit

 
We are going to grab all the buttons:
 
const​ accordionButtons
​ ​ = document.querySelectorAll(".accordion__button");
​ ​ ​ ​
 
Then we are going to use forEach method24 to “loop” through every item in the NodeList:  
 
​ ​
accordionButtons.forEach((button, ​ ​ ​ index)​ ​ => {
// do things to every button here
});
 
In the callback, notice that I am not only grabbing the current value (the button), but I am also 
grabbing the index. You’ll see why in a bit. 
 
Inside the callback, I am going to grab the aria-controls
​ ​ attribute of the button. This value 
matches the id​ ​ of the panel it’s associated with. Therefore, I can use that to toggle attributes on 
both the button and the associated panel. I’ll create a variable called ariaControls and 
associatedPanel. 
 
​ ​
accordionButtons.forEach((button, ​ ​ ​ index) ​ ​ => {
​const​ ​ariaControls​ = button.getAttribute("aria-controls");
​ ​ ​ ​ ​ ​
​const​ ​associatedPanel​ = document.getElementById(ariaControls);
​ ​ ​ ​
});
 
Then, still inside the callback, we will add a click event listener for every button. That’s when the 
toggling magic happens. 
 
​ ​
accordionButtons.forEach((button, ​ ​ ​ index) ​ ​ => {
​const​ ​ariaControls​ = button.getAttribute("aria-controls");
​ ​ ​ ​ ​ ​
​const​ ​associatedPanel​ = document.getElementById(ariaControls);
​ ​ ​ ​

​button.addEventListener("click",
​ ​ ​ ​ ​ () => {
​const​ ​ariaExpanded​ =
​button.getAttribute("aria-expanded")
​ ​ ​ ​ ​ === "true"
​ ​ ? true
​ ​ : false;
​ ​

​if​ (ariaExpanded)
​ ​ {
​button.setAttribute("aria-expanded",
​ ​ ​ ​ ​ false);
​ ​
​associatedPanel.setAttribute("hidden",
​ ​ ​ ​ ​ "");
​ ​
} ​else​ {
​button.setAttribute("aria-expanded",
​ ​ ​ ​ ​ true);
​ ​

24
"NodeList.prototype.forEach() - Web APIs - MDN Web Docs." 18 Mar. 2020, 
https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach.​ Accessed 31 Oct. 2020. 

76 
~ llh\.!,/lindsey
1Jl
wit

​associatedPanel.removeAttribute("hidden");
​ ​ ​ ​ ​
}
});
});
 
Note: Before I go into the if statement, I create a variable using a ternary operator25 to get a true 
and false boolean. The aria-expanded​ ​ value returns a string even though it appears to be a 
boolean. This helps me use the attribute as a boolean. 
 
Next, we add a keydown event listener and listen for the up arrow, down arrow, home, and end 
keys. According to the spec26, we need to add the following keyboard interactions when we are 
focused on the Accordion buttons: 
● On the up arrow, we will go to the previous accordion button. If we are on the first 
accordion button, we will go to the last accordion button. 
● On the down arrow, we will go to the next accordion button. If we are on the last accordion 
button, we will go to the first accordion button. 
● On the home key, we will go to the first accordion button. 
● On the end key, we are going to the last accordion button. 
 
Because this requires a lot of logic, I will set these key names as variables outside of the forEach 
callback. 
 
const​ arrowUp
​ ​ = "ArrowUp";
​ ​
const​ arrowDown
​ ​ = "ArrowDown";
​ ​
const​ home ​ ​ = "Home";
​ ​
const​ end ​ ​ = "End";
​ ​
 
Back inside the forEach callback, we want to create a switch statement. We will evaluate the e.key ​  
in the switch statement. For the up and down arrows, we'll send focus to different buttons based 
on the button's position in the accordion. This is why we need the index inside the callback: to help 
measure the position of the buttons. 
 
​ ​
accordionButtons.forEach((button, ​ ​ ​ index)
​ ​ => {
// the toggle logic

​button.addEventListener("keydown",
​ ​ ​ ​ ​ (e)
​ ​ => {
​switch​ (e.key)
​ ​ ​ ​ {

25
"Conditional (ternary) operator - JavaScript | MDN." 29 Jun. 2020, 
https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Conditional_Operator.​ Accessed 2 
Nov. 2020. 
26
"Accordion Example | WAI-ARIA Authoring Practices 1.1." 
https://www.w3.org/TR/wai-aria-practices-1.1/examples/accordion/accordion.html.​ Accessed 2 Nov. 2020. 

77 
~ llh\.!,/lindsey
1Jl
wit

​case​ ​arrowUp:

​break;​
​case​ ​arrowDown:

​break; ​
}
});
});
 
For the arrowUp case, we need to check if it’s the first item in the NodeList. If it’s the first item, 
then we want to send the focus to the last button. We can do that by taking the length of the 
NodeList and subtracting one (as the index starts at 0). Because we’ll need to use the last item a 
few times here, let’s put it in a variable outside the forEach callback. 
 
const​ lastIndex
​ ​ = accordionButtons.length
​ ​ ​ ​ - 1;
 
Then for the case of arrowUp, we can add in that logic: 
 
switch​ (e.key) ​ ​ ​ ​ {
​case​ ​arrowUp: ​
​if​ (index
​ ​ === 0) {
​accordionButtons[lastIndex].focus();
​ ​ ​ ​ ​
} ​else​ {
​accordionButtons[index ​ ​ ​ - 1].focus();
​ ​
}
​break; ​
}
 
If the index doesn’t equal 0, it can go to the previous item in the NodeList. 
 
We’ll do very similar logic for arrowDown, but the opposite: 
 
switch​ (e.key) ​ ​ ​ ​ {
​case​ ​arrowUp: ​
​if​ (index ​ ​ === 0) {
​accordionButtons[lastIndex].focus();
​ ​ ​ ​ ​
} ​else​ {
​accordionButtons[index ​ ​ ​ - 1].focus();
​ ​
}
​break; ​
​case​ ​arrowDown: ​
​if​ (index ​ ​ === lastIndex)
​ ​ {
​accordionButtons[0].focus(); ​ ​ ​

78 
~ llh\.!,/lindsey
1Jl
wit

} ​else​ {
​accordionButtons[index
​ ​ ​ + 1].focus();
​ ​
}
​break; ​
}
 
The Home button and the End button don’t have any conditionals. They’ll always focus on the same 
element when they are pressed. 
 
switch​ (e.key)
​ ​ ​ ​ {
​case​ ​arrowUp: ​
​if​ (index
​ ​ === 0) {
​accordionButtons[lastIndex].focus(); ​ ​ ​ ​ ​
} ​else​ {
​accordionButtons[index ​ ​ ​ - 1].focus();
​ ​
}
​break; ​
​case​ ​arrowDown: ​
​if​ (index
​ ​ === lastIndex)
​ ​ {
​accordionButtons[0].focus(); ​ ​ ​
} ​else​ {
​accordionButtons[index ​ ​ ​ + 1].focus();
​ ​
}
​break; ​
​case​ ​home: ​
​accordionButtons[0].focus(); ​ ​ ​
​break; ​
​case​ ​end: ​
​accordionButtons[lastIndex].focus(); ​ ​ ​ ​ ​
​break; ​
}
 
So there you have it! We’ve hand-coded an accessible accordion 
 
Final code: bit.ly/codepen-a11y-accordion
​  
 
Source: W3C - Accordion Example bit.ly/w3c-accordion ​  
 

79 
~ llh\.!,/lindsey
1Jl
wit

6.3 Tabs 
Visually, the tabs pattern are almost like layered bookmarks to certain tidbits of information. Most 
people think about clicking on a tab to show information, without considering how this information 
is communicated on assistive technology or how you would interact with it on a keyboard. 
 
Let’s go over what the expected pattern is in plain language:  
● Use the Right and left arrow keys to focus on different tabs.  
● If automatic activation is desired, the associated panel will appear when we focus on that 
tab. 
● If we have manual activation, we have to use the space or enter key to select a the tab and 
show the associated panel. 
● Use the tab key (sorry to be confusing) to go into the select panel. 
● You can’t go into panels that aren’t selected. 
 
 
Now let’s hand code it (I am using the manual pattern). 
 
First let’s create the shell of our HTML* to W3C-WAI standards.27 
 
*Note: I use progressive enhancement methodology to add a lot of the ARIA attributes, which you’ll 
see in the CodePen at the end. This is what the rendered HTML will look like. 
 

<div class="tabs">​ ​ ​
​ ​
<div ​ ​role="tablist" ​ ​
​ aria-label="Entertainment">
​ ​
<button
​ ​
​role="tab"
​aria-selected="true" ​ ​
aria-controls ​="nils-tab" ​
​ ​
​id="nils"
​ >
Nils Frahm
</button>
<button
​ ​
​role="tab"
aria-selected ​="false" ​
​ ​
​aria-controls="agnes-tab"
​ ​
​id="agnes"
​tabindex="-1" ​ ​

27
"Example of Tabs with Manual Activation | WAI-ARIA Authoring ...." 
https://www.w3.org/TR/wai-aria-practices-1.1/examples/tabs/tabs-2/tabs.html.​ Accessed 2 Nov. 2020. 

80 
~ llh\.!,/lindsey
1Jl
wit

>
Agnes Obel
</button>
<button
role ​="tab" ​
aria-selected ​="false" ​
aria-controls ​="complexcomplex" ​
​ ​
​id="complex"
​tabindex="-1" ​ ​
>
Joke
</button>
</div>
​ ​
<div ​ ​id="nils-tab" ​ ​
​ ​tabindex="0" ​ ​
​ role="tabpanel"
​ ​ ​
​ aria-labelledby="nils">
​ ​
<h2> ​Nils Frahm</h2> ​
<p>
Nils Frahm is a German musician, composer and record producer based in
Berlin. He is known for combining classical and electronic music and
for an unconventional approach to the piano in which he mixes a grand
Piano, upright piano, Roland Juno-60, Rhodes piano, drum machine, and
Moog Taurus.
</p>
</div>
<div
​ ​
​id="agnes-tab"
​tabindex="0" ​ ​
​ ​
​role="tabpanel"
​aria-labelledby="agnes" ​ ​
​hidden
​>
<h2> ​Agnes Obel</h2> ​
<p>
Agnes Caroline Thaarup Obel is a Danish singer/songwriter. Her first
album, Philharmonics, was released by PIAS Recordings on 4 October
2010 in Europe. Philharmonics was certified gold in June 2011 by the
Belgian Entertainment Association (BEA) for sales of 10,000 Copies.
</p>
</div>
<div
​ ​
​id="complexcomplex"
​tabindex="0" ​ ​
​ ​
​role="tabpanel"

81 
~ llh\.!,/lindsey
1Jl
wit

​ ​
​aria-labelledby="complex"
​hidden
​>
<h2> ​Joke</h2>

<p> ​Fear of complicated buildings:</p> ​
<p> ​A complex complex complex.</p> ​
</div>
</div>
 
Now we want to grab all the tab buttons and use the forEach method to “loop” through every one 
of them: 
 
const​ tabs
​ ​ = document.querySelectorAll('[role="tab"]');
​ ​ ​ ​

​ ​
tabs.forEach((tab, ​ ​ ​ index) ​ ​ => {
​// logic goes here
});
 
The first thing I want to do is add the arrow key navigation. The arrows are kinda like a mini focus 
trap in the sense that when you use the right arrow on the last tab you go to the first tab and vice 
versa. I like to store the value conditionally in a variable using a ternary operator28. 
 
​ ​
tabs.forEach((tab, ​ ​ ​ index) ​ ​ => {
​const​ ​nextTab​ = index ​ ​ === tabs.length
​ ​ ​ ​ - 1 ? tabs[0]
​ ​ ​ ​ ​
: tabs[index ​ + 1];
​const​ ​prevTab​ = index ​ ​ === 0 ? tabs[tabs.length
​ ​ ​ ​ ​ ​ - 1] : tabs[index
​ ​ ​ ​ - 1];
});
 
In the nextTab variable, I am using tabs (the NodeList, not the current item in the forEach callback), 
and I say if the index is the last one, the nextTab variable will be the first tab (tabs[0]) ​ ​ otherwise, 
it’ll be the next Tab in the node list. We do the opposite for the prevTab variable.  
 
Now we want to add a keydown listener for those tabs and listen for the right and left arrows: 
 
​ ​
tabs.forEach((tab, ​ ​ ​ index) ​ ​ => {
// other code
​tab.addEventListener("keydown",
​ ​ ​ ​ ​ (e)
​ ​ => {
​if​ (e.key​ ​ ​ ​ === "ArrowLeft")
​ ​ {
// logic
} ​else if​ (e.key ​ ​ ​ ​ === "ArrowRight")
​ ​ {

28
"Conditional (ternary) operator - JavaScript - MDN Web Docs." 29 Jun. 2020, 
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator.​  
Accessed 31 Oct. 2020. 

82 
~ llh\.!,/lindsey
1Jl
wit

// logic
}
});
});
 
We want to do three things when we use these keys. We want to focus on either the previous or 
next tab (why we set those variables earlier), we want to remove the tabindex attribute from the 
previous or next tab because we want that to be in the tab order, and we want to remove the 
current tab (the one we’ll be navigating away from) from the tab order and set the tabindex to -1. 
 
​ ​
tabs.forEach((tab, ​ ​ ​ index)
​ ​ => {
// other code
​tab.addEventListener("keydown",
​ ​ ​ ​ ​ (e)​ ​ => {
​if​ (e.key​ ​ ​ ​ === "ArrowLeft")
​ ​ {
​prevTab.focus();
​ ​ ​
​prevTab.removeAttribute("tabindex");
​ ​ ​ ​ ​
​tab.setAttribute("tabindex",
​ ​ ​ ​ ​ -1);
} ​else if​ (e.key ​ ​ ​ ​ === "ArrowRight")
​ ​ {
​nextTab.focus();
​ ​ ​
​nextTab.removeAttribute("tabindex");
​ ​ ​ ​ ​
​tab.setAttribute("tabindex",
​ ​ ​ ​ ​ -1);
}
});
});
 
Now if we are focused on one of the button tabs and press on the right or left arrows, the focus 
should circle around the tabs. 
 
Now we need to handle the logic when we click on the tab. When we click on the tab, it becomes 
the selected tab. Then that tab’s content becomes visible and the rest of the panels become 
hidden. This is also why we use buttons for the tabs because when we add a click event listener, 
we will get enter and space keydown events built in. 
 
First we’ll have to check if the current panel is hidden or not, because if it is hidden that’s when we 
will want to switch the tabs. Outside of the event, we want to grab the associated panel. We can 
use the ​aria-controls​ attribute to get the ​id​ of the panel, since those attributes’ values match: 
 
​ ​
tabs.forEach((tab, ​ ​ ​ index) ​ ​ => {
// other code
​const​ ​ariaControls​ = tab.getAttribute("aria-controls");
​ ​ ​ ​ ​ ​
const ​ ​panel​ = document.getElementById(ariaControls); ​ ​ ​ ​

83 
~ llh\.!,/lindsey
1Jl
wit

​tab.addEventListener("click",
​ ​ ​ ​ ​ () => {
​const​ ​panelIsHidden​ =
​panel.getAttribute("hidden")
​ ​ ​ ​ ​ === null
​ ​ ? false
​ ​ :​ true;​
});
});
 
Then, if the panelIsHidden
​ ​ we will need to change that to no longer be hidden, and set whatever 
was not hidden to be hidden. We’ll also need to set the new tab to be aria-selected
​ ​ and set 
whatever was previously selected to aria-selected
​ ​ as false. 
 
​ ​
tabs.forEach((tab, ​ ​ ​ index)
​ ​ => {
// other code
​const​ ​ariaControls​ = tab.getAttribute("aria-controls");
​ ​ ​ ​ ​ ​
const​ ​panel​ = document.getElementById(ariaControls);
​ ​ ​ ​

​tab.addEventListener("click",
​ ​ ​ ​ ​ () => {
​const​ ​panelIsHidden​ =
​panel.getAttribute("hidden")
​ ​ ​ ​ ​ === null
​ ​ ? false
​ ​ :​ true;

​if​ (panelIsHidden)
​ ​ {
​const​ ariaSelected
​ ​ = document.querySelector('[aria-selected="true"]');
​ ​ ​ ​
​const​ notHiddenId
​ ​ = ariaSelected.getAttribute("aria-controls");
​ ​ ​ ​ ​ ​
​const​ notHiddenPanel
​ ​ = document.getElementById(notHiddenId);
​ ​ ​ ​
​ariaSelected.setAttribute("aria-selected",
​ ​ ​ ​ ​ false);
​ ​
​ariaSelected.setAttribute("tabindex",
​ ​ ​ ​ ​ -1);
​tab.setAttribute("aria-selected",
​ ​ ​ ​ ​ true);
​ ​
​tab.removeAttribute("tabindex");
​ ​ ​ ​ ​
​panel.removeAttribute("hidden");
​ ​ ​ ​ ​
​notHiddenPanel.setAttribute("hidden",
​ ​ ​ ​ ​ "");
​ ​
}
});
});
 
Now we have the proper interactions for our tab panels! 
 
Final code: bit.ly/codepen-tabs
​  
 
Sources:  
● WAI-ARIA Authoring Practices - Tab Panel bit.ly/w3c-tab-spec ​  
● W3C - Example of Tabs with Automatic Activation bit.ly/w3c-tabs-automatic ​  
● W3C - Example of Tabs with Manual Activation (this is the example I used) 
bit.ly/w3c-tabs-manual 

84 
~ llh\.!,/lindsey
1Jl
wit

6.4 Live Regions 


We talked a bit about the live region pattern in the ARIA attributes chapter. When I say “Live 
Regions,” I mean any region where content can be updated dynamically. Pages don’t refresh when 
the content is updated. I’ve used live regions for toasts and error messages. We want to use them 
when you want to let the user know about some information quickly. They have a lot of use cases, 
particularly in dynamic JavaScript applications. When web applications are dynamic, we don’t 
always want to shift away from what we are doing. For example, if you are on Twitter, you may 
want updates that new tweets are available without moving away from what we were doing. 
 
First let’s start with the HTML from Chapter 5: 
 
​ ​
<div​ ​id="status" ​ ​
​ ​aria-live="polite"></div> ​
​ ​
<form​ ​id="email-form"> ​
​ ​
<label ​ ​for="email-field">Email:</label> ​ ​ ​
<input ​ ​type="email" ​ ​ ​ ​
​ ​id="email-field" ​ ​/>
​ ​
<button ​ ​type="submit">Submit</button> ​ ​ ​
</form>
 
Now let’s get the status element and store it in a variable. 
 
const​ status
​ ​ = document.getElementById("status");
​ ​ ​ ​
 
Then, let’s get the form: 
 
const​ form ​ ​ = document.getElementById("email-form");
​ ​ ​ ​
 
Create a variable that stores the message*: 
 
const​ confirmSubmit
​ ​ =
​ ​ "Your
​ email has been submitted!"; ​
 
*Note between let and const29. If you want this message to be dynamic, you should define the 
variable using let and change it based on what’s needed. Here we are using const because the 
confirmation message will always be the same. 
 
Now we can create an event listener on form submit. 
 
​ ​
form.addEventListener("submit", ​ ​ ​ (e)​ ​ => {
​// code will go here

29
"Var, Let, and Const – What's the Difference? - freeCodeCamp." 2 Apr. 2020, 
https://www.freecodecamp.org/news/var-let-and-const-whats-the-difference/.​ Accessed 31 Oct. 2020. 

85 
});
 
We will preventDefault on the form because we don’t want the browser to refresh. 
 
​ ​
form.addEventListener("submit",​ ​ ​ (e)
​ ​ => {
​e.preventDefault();
​ ​ ​
});
 
Then we will take the status variable (the element) and set it to the confirmSubmit
​ ​ string.  
 
​ ​
form.addEventListener("submit", ​ ​ ​ (e)
​ ​ => {
​e.preventDefault();
​ ​ ​
​status.innerText
​ ​ ​ = confirmSubmit;
​ ​
});
 
Let’s use my a11ywithlindsey email address to test 

Email: hell o@la11ywi1hlin d sey.co m Submit

 
Then when we press submit, notice how the focus stays on the submit, but the announcement 
happens: 
 

86 
You~ ~mai] has b~n _ubmittecl!__ _
Email. hello@a11ywithlmdsey.com ..,
________...__.,..

 
 
Resources: 
● MDN - ARIA Live Regions bit.ly/mdn-aria-live
​  
● Codesandbox React Example bit.ly/codesandbox-react-aria-live
​  

6.5 Combobox 
What is a combobox? In the simplest terms, it’s a widget with 2 main components: A textbox and a 
popup that helps us define the textbox. Back in Chapter 2, I said that “Select is not the same as a 
combobox.” There are a lot of folks who may agree or disagree with me on that. The reason I feel 
that is because the combobox has an inconsistent pattern and select has a clear one. Another 
reason is because comboboxes take a fair amount of JavaScript to make them accessible. The select 
element works out of the box. 
 
Here’s a few of the patterns that I’ve seen among comboboxes: 
● Select Only Combo (acts like a select list but styled)30 
● No Autocomplete31 

30
"Select-Only Combobox Example - W3C on GitHub." 
https://w3c.github.io/aria-practices/examples/combobox/combobox-select-only.html.​ Accessed 31 Oct. 2020. 
31
"Editable Combobox without Autocomplete Example - W3C on ...." 
https://w3c.github.io/aria-practices/examples/combobox/combobox-autocomplete-none.html.​ Accessed 31 
Oct. 2020. 

87 
~ llh\.!,/lindsey
1Jl
wit
32
● Autocomplete with manual selection  
● Autocomplete with automatic selection33 
 
These combinations don't have consistent support among browsers, operating systems, and 
assistive devices. The reason why I am not hand-coding this pattern is that the examples I saw on 
w3c didn't work for me on VoiceOver. If you ever decide to hand-code a select list, use with caution 
and be sure to test manually and with disabled users. 
 
I usually rely on a library like Downshift to create accessible comboboxes. When I code a 
combobox, I care most about replicating the <select> pattern as closely as possible. These are the 
patterns I try to replicate closely: 
● Opens on the Up arrow, Down arrow, Space and Enter keys 
● Listens for the letters you type and goes to the first matching option 
● Announces the number of options upon open 
● Uses the arrow keys to navigate between the options 
● Uses the Space or Enter key to select the option  
 
I've created two examples of this, and the one not using Downshift is pretty limited in capabilities. 
 
● Codesandbox - Downshift Demo bit.ly/sandbox-downshift
​  
● CodePen - bit.ly/a11y-mediocre-combox
​  
 
Because there's a LOT of factors here, I recommend that if you need a combobox in a form, just use 
a select element. If not, I recommend using a library because my hand-coded example doesn't 
cover everything you need to cover. And regardless, always test. 

6.6 Hamburger Navigations 


At the time of this writing, there's not a ton of information about creating accessible hamburger 
navigations. There's nothing from the W3C-WAI indicating best practices on using ARIA in the best 
way. When I'm in this situation, I usually take a few of the best focus management practices and 
apply them. So here's what I have come up with from some research and what I know about 
common accessible patterns: 
● We want to make sure that the menu closes on escape and goes back to the trigger that 
opened the link. 
● We want to focus on the first focusable item in the menu list upon open. 
● We want to make sure that there's a button to close the menu. 

32
"Editable Combobox With List Autocomplete Example | WAI ...." 
https://w3c.github.io/aria-practices/examples/combobox/combobox-autocomplete-list.html.​ Accessed 31 
Oct. 2020. 
33
"Editable Combobox With Both List and Inline Autocomplete ...." 
https://w3c.github.io/aria-practices/examples/combobox/combobox-autocomplete-both.html.​ Accessed 31 
Oct. 2020. 

88 
~ llh\.!,/lindsey
1Jl
wit

● We want to make sure there's some discernible text inside the open menu button, even if 
it's visually hidden.  
● We shouldn't be able to focus on links when the menu is closed. This is important when we 
have our menu positioned off-screen instead of doing display none. 
● We want to put our nav tags around the button to communicate that we are entering the 
navigation. 
 
To do this, first, let's create the markup*: 
*Note: The CodePen example is written with Progressive Enhancement in mind, but we won’t be 
showing that here. This will be the rendered markup. If the CodePen looks a bit different, that’s 
why. 
 
<header>
​ ​ ​ ​ ​
<h1><a href="/">Logo</a></h1> ​
<nav>
​ ​
<button​ ​id="open-toggle" ​ aria-expanded="false"
​ ​ ​ ​ ​
​ ​aria-controls="menu">

​ ​
<span​ ​class="hamburger-wrapper" ​ aria-hidden="true">
​ ​ ​ ​
​ ​
<span​ ​class="hamburger-line"></span> ​
​ ​
<span​ ​class="hamburger-line"></span> ​
​ ​
<span​ ​class="hamburger-line"></span> ​
</span>
​ ​
<span​ ​class="sr-only">Menu</span> ​ ​ ​
</button>
​ ​
<div​ ​id="menu" ​ ​
​ class="menu-wrapper">
​ ​
<ul​ ​aria-labelledby="open-toggle" ​ ​ ​ ​
​ ​class="menu"> ​
​ ​
<li><a​ ​href="/">Home</a></li> ​ ​ ​
​ ​
<li><a​ ​href="#about">About</a></li> ​ ​ ​
​ ​
<li><a​ ​href="#contact">Contact</a></li> ​ ​ ​
</ul>
​ ​
<button​ ​id="close-toggle" ​ ​
​ ​class="no-js">Close ​ ​ ​
Menu</button>
</div>
</nav>
</header>
<main>
<!-- sections with landmarks -->
</main>
 
Heads up, this will not look well-styled. The goal here is to get the functionality. 
 
Before we do anything else, I want to create a variable that we will toggle true and false called 
isOpen.​ We don’t need to do this, but I find that it makes the logic easier for me if I have a state. 
 

89 
~ llh\.!,/lindsey
1Jl
wit

let​ ​isOpen​ ​=​ ​false; ​


 
I’m using let ​ ​ here instead of const
​ ​ because we will reassign the isOpen
​ ​ variable. 
 
Next we’ll grab the open and close button. 
 
const​ buttonToggle
​ ​ = document.getElementById("open-toggle");
​ ​ ​ ​
const​ closeButton
​ ​ = document.getElementById("close-toggle");
​ ​ ​ ​
 
We'll also add some CSS for if the menu is collapsed. I like to use ARIA states to style instead of 
toggling classes: 
 

#open-toggle ​ {
​height: ​ 30px;
​ ​
​width: ​ 40px;
​ ​
​position: ​ relative;
​ ​
}


.hamburger-wrapper ​ {
​position: ​ absolute;
​ ​
​top:
​ 3px;
​ ​
​left:​ 4px;
​ ​
}


.hamburger-line ​ {
​position: ​ absolute;
​ ​
​height: ​ 4px;
​ ​
​width:
​ 30px;
​ ​
​background: ​ black;
​ ​
​top:
​ 7px;
​ ​
}

​ ​ ​
.hamburger-line:first-child​ {
​top:
​ 0;
​ ​
}

​ ​ ​
.hamburger-line:last-child​ {
​top:
​ 14px;
​ ​
}

​ ​ ​ ​ ​ ​ ​
​ + .menu-wrapper
#open-toggle[aria-expanded="false"] ​ {
​display:
​ none;
​ ​

90 
~ llh\.!,/lindsey
1Jl
wit

}
 
Remember, I’m not making this fancy. This menu looks pretty ugly. Our goal is to make it 
functional and accessible. 
 
Now let’s add the click event to the button toggle and toggle the isOpen ​ ​ variable we set at the 
beginning: 
 
​ ​
buttonToggle.addEventListener("click", ​ ​ ​ () => {
​isOpen​ = !isOpen;
​ ​
});
 
Next, we’ll use this state to decide how we want to set the aria-expanded
​ ​ attribute.  
 
​ ​
buttonToggle.addEventListener("click", ​ ​ ​ () => {
​isOpen​ = !isOpen;
​ ​
​if​ (isOpen)
​ ​ {
​buttonToggle.setAttribute("aria-expanded",
​ ​ ​ ​ ​ true);
​ ​
} else {
​buttonToggle.setAttribute("aria-expanded",
​ ​ ​ ​ ​ false);
​ ​
}
});
 
Additionally, when the state isOpen, ​ ​ we want to focus on the first element in the menu. Outside of 
the event listener, we will grab all the links. Back inside the event listener, when the menu is open, 
we will focus on the first focusable element. 
 
const​ menuWrapper
​ ​ = document.querySelector(".menu-wrapper");
​ ​ ​ ​
const​ menuLinks
​ ​ = menuWrapper.querySelectorAll("a");
​ ​ ​ ​ ​ ​

​ ​ ​ ​
buttonToggle.addEventListener("click", ​ () => {
​isOpen​ = !isOpen;
​ ​
​if​ (isOpen)
​ ​ {
​buttonToggle.setAttribute("aria-expanded",
​ ​ ​ ​ ​ true);
​ ​
​requestAnimationFrame(()
​ ​ ​ ​
=> menuLinks[0].focus()); ​
} else {
​buttonToggle.setAttribute("aria-expanded",
​ ​ ​ ​ ​ false);
​ ​
}
});
 
Note: we are using requestAnimationFrame as a workaround for a VoiceOver bug. We also used this 
in the Modal section. 

91 
~ llh\.!,/lindsey
1Jl
wit

 
Now, when the menu is open, we want to close the menu on escape. But we only want to listen for 
that key if the menu is open. Let’s first create a function to do this: 
 
function​ closeOnEsc(e)
​ ​ ​ ​ {
​if​ (e.key
​ ​ ​ ​ === "Escape")
​ ​ {
// perform some logic
}
}
 
What are some things we want to happen if we press the escape key: 

● Focus back to the button 


● Set the aria-expanded
​ ​ attribute on the button to false (which also controls the styling) 
● Set the isOpen
​ ​ state to false 

function​ closeOnEsc(e)
​ ​ ​ ​ {
​if​ (e.key
​ ​ ​ ​ === "Escape")
​ ​ {
​buttonToggle.setAttribute("aria-expanded",
​ ​ ​ ​ ​
​ false);

​buttonToggle.focus(); ​ ​ ​
​isOpen​ = false;
​ ​
}
}
 
Additionally, we want to do this same logic if we press the close button, so let’s separate it into a 
function: 
 
function​ closeMenu()
​ ​ {
​buttonToggle.setAttribute("aria-expanded",
​ ​ ​ ​ ​
​ false);

​buttonToggle.focus(); ​ ​ ​
​isOpen​ = false; ​ ​
}

function​ closeOnEsc(e)
​ ​ ​ ​ {
​if​ (e.key
​ ​ ​ ​ === "Escape")
​ ​ {
​closeMenu(); ​
}
}
 
Now let’s add that to the close button 
 
​ ​
closeButton.addEventListener("click", ​ ​ ​ () => {
​closeMenu(); ​

92 
~ llh\.!,/lindsey
1Jl
wit

});
 
Now let’s add and remove that event listener from the window in the button toggle event listener. 
Additionally, we’ll need to remove the window event listener in the closeMenu
​ ​ function:

function​ closeMenu()
​ ​ {
window. ​removeEventListener("keydown",
​ ​ ​ closeOnEsc);
​ ​
​buttonToggle.setAttribute("aria-expanded",
​ ​ ​ ​ ​
​ false);

​buttonToggle.focus();
​ ​ ​
​isOpen​ = false;
​ ​
}

​ ​
buttonToggle.addEventListener("click",​ ​ ​ () => {
​isOpen​ = !isOpen;
​ ​
​if​ (isOpen)
​ ​ {
window. ​addEventListener("keydown",
​ ​ ​ closeOnEsc);
​ ​
​buttonToggle.setAttribute("aria-expanded",
​ ​ ​ ​ ​ true);
​ ​
​requestAnimationFrame(()​ ​ ​
=> menuLinks[0].focus()); ​ ​
} else {
window. ​removeEventListener("keydown", ​ ​ ​ closeOnEsc);
​ ​
​buttonToggle.setAttribute("aria-expanded",
​ ​ ​ ​ ​ false);
​ ​
}
});
 
Final code: bit.ly/codepen-hamburger-nav
​  
 
Sources: A11y Matters - Accessible Mobile Navigation bit.ly/a11y-matters-mobile-nav
​  

6.7 Client Side Routing 


Many large scale applications use JavaScript frameworks to create their robust applications. 
Usually, these applications are Single Page Apps with client-side routing. While this isn't my area 
of expertise, it's critical to talk about why it can be problematic for disabled users. I also want to 
briefly touch on the research that exists and how we can start to think through our routing. 
 
Client-side routing is when JavaScript handles routing through the app by controlling the browser's 
history and mapping client-rendered URLs to each page or view.34 Client-side
​ routing is a blessing 
for the speed and potentially a curse for accessibility. When we have a page change using 
client-side routing, the page isn't refreshing with a new page. Instead, it's dynamically updating. 
Because we aren't waiting for the server to grab content, client-side routing is SUPER fast. But now 

34
"What we learned from user testing of accessible client-side ...." 11 Jul. 2019, 
https://www.gatsbyjs.com/blog/2019-07-11-user-testing-accessible-client-routing/.​ Accessed 31 Oct. 2020. 

93 
~ llh\.!,/lindsey
1Jl
wit

we have to be sure to create an experience that navigates disabled users throughout the 
application. 
 
In 2019, Marcy Sutton did some user research on some of the common techniques to determine 
what was the best user experience universally for client-side routing. What she found wasn't what I 
would have expected. I'd expect you to shift focus back to the top of the page and announce the 
new page change since it's the most similar to default browser behavior. Instead, she found that 
it's best to focus on the new content and then give an option to go back to the navigation using a 
skip link. Additionally, you should announce the new page title with a live region.  
 
I'd recommend reading the research findings. If you are using client-side routing, you can begin to 
apply the techniques.  
 
Source: What we learned from user testing of accessible client-side routing techniques with Fable 
Tech Labs bit.ly/client-side-routing-research-marcy-sutton
​  
 
Additional reading: 
● Patterns & Strategies for accessible web-apps - An accessible routing pattern 
bit.ly/vue-a11y-routing 
● Short Div - Client Side A11y bit.ly/short-div-client-side-a11y
​  

6.8 React Libraries and Resources 


If you don't use React, you can skip this section. But in my professional life, I code in React. I want 
to go over some of the libraries that I use. This section will be pretty brief, as I already explained 
how I configure these libraries in my blog post. bit.ly/awl-react-libraries
​  

● React Modal is a library owned by ReactJS. It aims to be fully accessible.35  


● React Burger Menu which is an open-source library that animates in a hamburger menu.36 
● React Accessible Accordion which is what it says, an accessible accordion.37 
● React Tabs is a library owned by ReactJS. It aims to be fully accessible.38  
● Downshift is a library with primitives to build simple, flexible, WAI-ARIA compliant React 
autocomplete, combobox, or select dropdown components.39 ​I didn't go over this one in my 
blog post, but if you're interested in seeing a usage example, I created a codesandbox: 
bit.ly/sandbox-downshift 

35
"reactjs/react-modal - GitHub." https://github.com/reactjs/react-modal.
​ ​ Accessed 31 Oct. 2020. 
36
​"negomi/react-burger-menu: An off-canvas sidebar ... - GitHub." 
https://github.com/negomi/react-burger-menu.​ Accessed 31 Oct. 2020. 
37
"springload/react-accessible-accordion: Accessible ... - GitHub." 
https://github.com/springload/react-accessible-accordion.​ Accessed 31 Oct. 2020. 
38
"reactjs/react-tabs - GitHub." https://github.com/reactjs/react-tabs.
​ ​ Accessed 31 Oct. 2020. 
39
"downshift-js/downshift: A set of primitives to build ... - GitHub." 
https://github.com/downshift-js/downshift.​ Accessed 31 Oct. 2020. 

94 
~ llh\.!,/lindsey
wit1Jl

   

95 
~ llh\.!,/lindsey
1Jl
wit

Chapter 7 - Progressive Enhancement 


What is progressive enhancement? I liked the perspective from A List Apart40 and Shopify.41 There's 
graceful degradation, which seems to be what most people focus on. Graceful degradation is 
building out the features with the latest and greatest. Then you test for the older browsers and 
make it "passable" there. 
 
Progressive enhancement focuses on content and a base level experience first. It doesn't focus on 
browser support because it's more focused on the content. 
 
If you're like me, when I first learned web development, you learned about HTML first, then CSS, 
and then JavaScript. You learned about the separation of concerns. HTML is the content layer, CSS 
is the presentation layer, and JavaScript is the behavior layer. Because the content is the HTML 
layer, we focus there first. We need to ask ourselve what HTML is absolutely necessary to 
understand this content? Then we focus on the presentation layer. We make sure we aren't hiding 
important content and using @supports42 to add in more modern CSS. Then we focus on 
interactivity in JavaScript. 
 
Why is Progressive Enhancement in the JavaScript portion of this book if it focuses on JavaScript 
last? Because to progressively enhance a website doesn't mean we hate JavaScript and don't ever 
want to use it. But we do need to check if it loads before we start adding interactive 
enhancements. You need to add interactive elements and ARIA states from the JavaScript instead 
of hardcoding them into HTML. This is because we don't want to tell people that they can interact 
with elements when JavaScript isn't loaded. 
 
My strategy: 
● Adding a no-js class to the HTML that gets removed first when JavaScript loads. 
● Add a noscript43 element​ to explain that there's an alternate version. 
● Think through the vital content of the interactive features. 
● Adding alternative styling if the JavaScript doesn't load using the no-js class. 
● Adding ARIA attributes via JavaScript, especially if it indicates interactivity. 
● Turn off JS and test it out. 
 
Let's build the accordion from the last chapter with progressive enhancement in mind. 
40
"Understanding Progressive Enhancement – A List Apart." 7 Oct. 2008, 
https://alistapart.com/article/understandingprogressiveenhancement/.​ Accessed 31 Oct. 2020. 
41
"What is Progressive Enhancement and Why Should You Care ...." 15 Feb. 2017, 
https://www.shopify.com/partners/blog/what-is-progressive-enhancement-and-why-should-you-care.​  
Accessed 31 Oct. 2020. 
42
"@supports - CSS: Cascading Style Sheets | MDN." 22 Jul. 2020, 
https://developer.mozilla.org/en/docs/Web/CSS/@supports.​ Accessed 2 Nov. 2020. 
43
"noscript - MDN Web Docs - Mozilla." 12 Apr. 2020, 
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/noscript.​ Accessed 31 Oct. 2020. 

96 
~ llh\.!,/lindsey
1Jl
wit

 
First, create the HTML scaffold: 
 
​ ​
<html​ ​lang="en"> ​
<head></head>
<body></body>
</html>
 
The we add the no-js class to the html element: 
 
​ ​
<html​ ​lang="en" ​ ​
​ ​class="no-js"> ​
<head></head>
<body></body>
</html>
 
Then we add the noscript element inside the body tag: 
 
<html​ ​lang="en"​ ​ ​ ​
​ ​class="no-js"> ​
<head></head>
<body>
​<noscript>
This page is loaded without JavaScript. To enable interaction, please    
​enable JavaScript
​</noscript>
</body>
</html>
 
Now let’s pause and think this through. For an accordion, what is the important content that we 
need to preserve? This is subjective, but this is what about for me: 
● Remove button functionality if JavaScript doesn’t load using CSS. 
● Add the button text as headings inside the panels. 
● Remove the hidden attribute from the panel. 
● Remove all the ARIA attributes. 
 
<html​ ​lang="en" ​ ​ ​ ​
​ ​class="no-js"> ​
<head></head>
<body>
​<noscript>
This page is loaded without JavaScript. To enable interaction, please    
​enable JavaScript
​</noscript>
​ ​ ​
​<div id="app"> ​

97 
~ llh\.!,/lindsey
1Jl
wit

<h1> ​Accordion</h1> ​
​ ​
<div ​ ​id="accordionGroup" ​ ​
​ ​class="accordion"> ​
<button ​ ​
​ ​id="accordion-button-1" ​ ​
​ ​class="accordion__button"> ​
Section 1
​</button>
<div
​ ​
​role="region"
​ ​
​id="accordion-section-1"
​ ​
​class="accordion__section"
​>
<h2> ​Section 1</h2> ​
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer
vitae lectus in sapien tempus maximus.
​</p>
</div>
<button ​ ​
​ ​id="accordion-button-2" ​ ​
​ ​class="accordion__button"> ​
Section 2
​</button>
<div
​ ​
​role="region"
​ ​
​id="accordion-section-2"
​ ​
​class="accordion__section"
​>
<h2> ​Section 2</h2> ​
<p>
Etiam laoreet hendrerit est non vulputate. Quisque lacinia justo
leo, at pretium nulla dapibus nec.
​</p>
</div>
<button ​ ​
​ ​id="accordion-button-3" ​ ​
​ ​class="accordion__button"> ​
Section 3
​</button>
<div
​ ​
​role="region"
​ ​
​id="accordion-section-3"
​ ​
​class="accordion__section"
​>
<h2> ​Section 3</h2> ​
<p>
Mauris in porta lacus, eget imperdiet nibh. Sed porttitor
Bibendum ornare.

98 
~ llh\.!,/lindsey
1Jl
wit

​</p>
​</div>
</div>
</div>
</body>
</html>
 
Let’s format the headers and footers the way we want to with the .no-js
​ ​ class 
 

.no-js ​ .accordion__button
​ ​ {
​display: ​ none;
​ ​
}


.no-js ​ .accordion__section
​ ​ {
​background: ​ transparent;
​ ​
​border: ​ 0; ​ ​
}
 
Now let's jump into JavaScript and remove that .no-js ​ ​ class and start styling up our accordion. 
 
​ ​ ​
document.documentElement.classList.remove("no-js"); ​ ​ ​ ​ ​
 
If there isn't a no-js ​ ​ class, I will give the headings the style display:
​ none.​ The buttons and the 
headings say the same thing, and they feel redundant. 
 
​ ​ ​
:root:not(.no-js) ​ ​ ​ h2 ​ ​ {
​display: ​ none;​ ​
}
 
If we wanted to take this one step further, we could add inline styling to the buttons instead of the 
CSS. That way, if the browser didn't load the stylesheets, we still wouldn't get those buttons. We 
could remove that styling with JavaScript. For this example, I am going to leave it as is. But I 
wanted to put that nugget of thinking in your head. 
 
Now, remember, we're using the same code we did before, so we don't have to redo our JavaScript. 
But we do want to add a few more things like all the attributes we need to the buttons and the 
sections. Those are important for styling. 
 
In the original code, we have an accordionButtons ​ ​ variable that we use forEach to cycle through 
the NodeList. We also have the ​associatedPanel​ variable inside the forEach callback, and we can 
use those to add the attributes we need. 
 

99 
~ llh\.!,/lindsey
1Jl
wit

On the accordion buttons, we need: 


● aria-expanded​ to default to false 
● aria-controls​ to be set to the id of the associated panel 
 
On the panel, we need: 
● All the panels to have the hidden ​ ​ attributes 
● the aria-labelledby
​ ​ attribute that matches the value of the button id. ​ ​ 
 
​ ​
accordionButtons.forEach((button, ​ ​ ​ index)
​ ​ => {
​button.setAttribute("aria-expanded",
​ ​ ​ ​ ​ false);
​ ​
​button.setAttribute("aria-controls",
​ ​ ​ ​ ​ `accordion-section-${index
​ ​ ​ ​ + 1}`);
​ ​ ​ ​
​const​ ​ariaControls​ = button.getAttribute("aria-controls");
​ ​ ​ ​ ​ ​
​const​ ​associatedPanel​ = document.getElementById(ariaControls);
​ ​ ​ ​
​associatedPanel.setAttribute("hidden",
​ ​ ​ ​ ​ "");
​ ​
​associatedPanel.setAttribute(
​ ​ ​
​"aria-labelledby", ​
​`accordion-button-${index ​ ​ ​ + 1}` ​ ​ ​
);

​// all the click and keydown events from the accordion code
});
 
Now the best thing to do is turn off JavaScript and see if it worked. 
 
To disable JavaScript in Chrome, you can use the Developer Tools44: 
1. Open up Chrome Developer Tools 
2. Type Command+Shift+P (Mac) or Control+Shift+P (Windows)  
3. Start typing JavaScript 
4. There is a “Disable JavaScript” option with a tag of debugger. Select that option 
5. Refresh.  
 
To disable JavaScript in Firefox: 
1. Open up a browser and type about:config
​  
2. You’ll get a screen asking you to proceed with caution. Accept the risk 
3. Search javascript in the search bar 
4. Double click to toggle javascript.enabled to false. 
5. Refresh. 
 
Final code: bit.ly/codepen-progressive-accordion
​  

44
"Disable JavaScript With Chrome DevTools | Google Developers." 14 Jul. 2020, 
https://developers.google.com/web/tools/chrome-devtools/javascript/disable.​ Accessed 2 Nov. 2020. 

100 
Chapter 8 - User Preferences 
While there are many standards, we must remember that access doesn't mean the same thing for 
everyone. In some scenarios, you prefer one accessibility preference. In other areas, you might 
choose something else. 
 
An example of this is dark mode. For the most part, I prefer dark mode to attempt to reduce my eye 
strain. But for any apps that I am taking notes or writing, I like light mode. This is why I don't rely 
only on operating system preferences without giving options. 
 
In Fall 2020, Google Docs updated their application on iOS. I noticed that all my documents were 
now in Dark Mode. There's something about word documents that felt wrong to me in Dark Mode. 
So I went to the user settings to see if I could switch it back. And I could. The default option 
matched my operating system, but I could easily change the theme to light, which is precisely what 
I did. 
~
< Theme

✓ Light

Dark

System Default

Operating system preferences 


The most common system preferences are for dark/light mode45 and reduce motion46. If you go into 
the system preferences for your operating system, there's likely many more. You'll likely see 
grayscale, reduce transparency, and high contrast mode (which I consider different from dark 
mode). As media queries and operating systems get more advanced, we can use them in our web 
applications! 
 
As of right now, the best support I've seen is for the prefers-color-scheme
​ ​ and 
prefers-reduce-motion​ media queries. 

45
"prefers-color-scheme - CSS: Cascading Style Sheets | MDN." 7 Jul. 2020, 
https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme.​ Accessed 26 Oct. 2020. 
46
"prefers-reduced-motion - CSS: Cascading Style Sheets | MDN." 16 Jul. 2020, 
https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion.​ Accessed 26 Oct. 2020. 

101 
~ llh\.!,/lindsey
1Jl
wit

 
A simple example of how I would use prefers-color-scheme: 
 
@media​ (prefers-color-scheme:
​ ​ dark)
​ ​ {
​body​ {
​background:​ #363636;
​ ​
​color:​ #fff;
​ ​
}
}

@media​ (prefers-color-scheme:
​ ​ light)
​ ​ {
​body​ {
​background: ​ #fff;
​ ​
​color: ​ #363636;
​ ​
}
}
 
All this does is invert the body color depending on what mode you are using on your operating 
system. 
 
Reducing motion can get a little bit more complicated, as some animations are pretty custom. 
In the following CodePen, I took an example of a highly animated button and created some reduce 
motion styling. bit.ly/codepen-reduce-motion
​  
 
There’s a lot of transitions going on in the animation:  
1. Letter spacing 
2. Background Opacity 
3. Borders 
4. Scale 
 
If you go to the codepen, hovering over it looks almost like it’s spinning, which could cause nausea 
or vertigo. I wish books supported gifs so I didn’t have to make you go to a CodePen to see the 
effect, but it is what it is. 
 
To remove the animations on this, I have to remove all those transitions: 
 
@media​ (prefers-reduced-motion:
​ ​ reduce)
​ ​ {
. ​btn​ ​span​ {
​transition: ​ unset;
​ ​
}

​ ​ ​
​.btn::before​ {

102 
~ llh\.!,/lindsey
1Jl
wit

​transition:
​ unset;
​ ​
}

​ ​ ​
​.btn::after ​ {
​transition:​ unset;
​ ​
}
}
 
What you have to remove may depend on what animations you have. You may be able to do so at 
the root level. Or you may need to get more granular and unset them at the element level. 
 
I saw other intriguing media queries, but they are either experimental or singular browser: 
● -ms-high-contrast47  
● prefers-reduced-transparency48  
● prefers-contrast49 
 
Hopefully, we can use these to make better accessibility preferences in the future. 
 
In this chapter, we'll be extending the prefers-color-scheme
​ ​ media query. This menu will 
default to operating system preferences but allow you to make a choice. Because this book is 
focused on the front end, we will be storing these in the browser's localStorage.
​ ​ Because we can 
50
access the storage object after we've closed the browser, it's a good option for something simple. 
For a more robust application, you may want to work with your backend developers about how 
you'd like to store the preferences. 
 
The first thing we want to do before we store our options is to create a fieldset of radio buttons 
and use those to get the color set. 
 
<fieldset>
​ ​
<legend ​ ​class="sr-only">Theme</legend>
​ ​ ​
<div>
​ ​
<input ​ ​id="system-default" ​ ​
​ ​type="radio" ​ ​
​ ​name="theme" ​ ​/>
​ ​
<label ​ ​for="system-default">System ​ ​ ​
Default</label>
</div>

47
"ms-high-contrast - MDN Web Docs - Mozilla." 12 Oct. 2020, 
https://developer.mozilla.org/en-US/docs/Web/CSS/@media/-ms-high-contrast.​ Accessed 31 Oct. 2020. 
48
"prefers-reduced-transparency - CSS: Cascading Style Sheets ...." 8 Sep. 2020, 
https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-transparency.​ Accessed 31 Oct. 
2020. 
49
"prefers-contrast - CSS: Cascading Style Sheets - MDN - Mozilla." 8 Sep. 2020, 
https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-contrast.​ Accessed 31 Oct. 2020. 
50
"Storage - Web APIs - MDN Web Docs - Mozilla." 26 Aug. 2020, 
https://developer.mozilla.org/en-US/docs/Web/API/Storage.​ Accessed 2 Nov. 2020. 

103 
~ llh\.!,/lindsey
1Jl
wit

<div>
​ ​
<input ​ ​id="light" ​ ​
​ ​type="radio" ​ ​
​ ​name="theme" ​ />
​ ​
​<label​ ​for="light">Light</label> ​ ​ ​
</div>
​<div>
<input ​ ​id="dark" ​ ​ ​ ​
​ ​type="radio" ​ ​
​ ​name="theme" ​ />
​ ​
​<label​ ​for="dark">Dark</label> ​ ​ ​
</div>
</fieldset>
 
Then we'll create an arbitrary variable called storageKey. ​ ​ Eventually, this is what we will name 
the item in ​localStorage.​  
 
const​ storageKey
​ ​ = "color-scheme";
​ ​
const​ storedColorScheme
​ ​ = localStorage.getItem(storageKey);
​ ​ ​ ​
 
Because we haven't done anything with localStorage, ​ ​ this value will be null.​ ​ If someone has 
never set their preferences, the system default will be the fallback. 
 
const​ colorPref
​ ​ = storedColorScheme
​ ​ ? storedColorScheme
​ ​ : "system-default";
​ ​
 
Notice that the "system-default" string matches the id ​ ​ of that radio. I did this on purpose so that I 
could do this: 
 
const​ selected​ ​ = document.getElementById(colorPref);
​ ​ ​ ​
selected.checked ​ ​ ​ = true;
​ ​
 
This code will set the selected radio button upon page load and add the checked state. Even 
though radio buttons aren't checkboxes, it means that it will be "filled." 
 
Now here's where the real magic happens. I am going to use the dataset property to create some 
data attributes51 on the body element. 
 
​ ​ ​
document.body.dataset.colorScheme ​ ​ ​ = colorPref;
​ ​
 
Adding this will give us some HTML that looks like this: 
 
<body​ ​data-color-scheme="system-default"> ​ ​ ​
<!-- children elements -->
</body>

51
"Using data attributes - Learn web ...." 10 Nov. 2019, 
https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes.​ Accessed 2 Nov. 2020. 

104 
 
Next, we will grab all our radio buttons and then add an event listener to them to toggle this 
attribute. Once we do that, we can go back to our CSS and start styling. 
 
const​ userPref
​ ​ = document.getElementById("user-preferences");
​ ​ ​ ​
const​ radiosWrapper
​ ​ = userPref.querySelector("fieldset");
​ ​ ​ ​ ​ ​
const​ radios
​ ​ = radiosWrapper.querySelectorAll('[type="radio"]');
​ ​ ​ ​ ​ ​

​ ​ ​ ​
radios.forEach((radio) ​ =>
​radio.addEventListener("click",
​ ​ ​ ​ ​ toggleColorScheme)
);
 
Now let's write that toggleColorScheme function. In this function, we get the attribute from this.
​ ​ 
52
Because this is a non-arrow function, this
​ ​ takes the context of what we clicked on. We'll use that 
to set that same data attribute. 
 
function​ toggleColorScheme()
​ ​ {
​ ​ ​
​colorScheme​ = this.getAttribute("id"); ​ ​ ​
document. ​body.dataset.colorScheme
​ ​ ​ ​ ​ = colorScheme;
​ ​
}
 
Now let's test that those attributes are toggling and use that data attribute to get to styling. 
!

•·-
Th@m@ <html lang="en"> event
Theme------■!_.►._<h~e~a~d~>1111..~-</head>
0 SystemDefault ___---~
Light ►<div i.d="user-preferences"> ... </div:>
►<p> ... </p>
Dark ► <p> ... </p>
► <p> ... </p>

► <script id="rendered-js"> ... </script>


Lorem ipsum dolor sit ~ </body>
dictum rnetus nee eleif~ </ht11l>
volutpat vitae vuJputatci
Donec et nibh ac libero:
,;uvL• rrc uLml_...

Them@ ! <hh1l lang="en"> event


Theme I ► <head> ··· </head>
N&MMYF?iiliiiilltidHMMHi¥4·ii·l4i;,rn
1
,tWiffiHWM
System Default · ► <div id="user-pre-ferences"> ... </div:>
0 Light. ► <p> ... </p>
► <p> ... </p>
Dark ► <p> ... </p>
► -<.script id=" rendered-j s"> ···</script>
~/hn1rh,~  

52
"this - JavaScript | MDN - MDN Web Docs - Mozilla." 2 Jun. 2020, 
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this.​ Accessed 2 Nov. 2020. 

105 
•<body c:lass=<"'' translate=<"no" data-color-sche111e="aark">
System Default ► <div .id="user-preferences"> ... </div:>
► <p> ... </p>
Light
► <p> ... </p>
Dark ► <p> ... </p>

► <script id="rendered-js"-. ··· </script>


 
 
It looks like it's doing what we need! 
 
This process could get confusing, so let's go over the goals again: 
1. User settings get honored over anything, regardless of what the operating system allows. 
2. System defaults will align with media query settings 
3. User settings will override media query settings  
 
For the most part, in this book, I use plain CSS, but in this instance, I am going to use SCSS.53 
Nesting will make this code easier to read. I'm also going to use CSS variables. Note: I did a little 
extra CSS and added a toggle menu, which I don't go over in this book, but you can take a peek at 
the CodePen at the end. 
 
So first, set the default settings if no user settings or operating system settings exist. For this 
exercise, my website will be in a dark mode if none of those settings exist. 
 

:root ​ {
--white: ​#fff; ​
--dark-grey: ​#363636; ​
}

body​ {
​font-family: ​ "Open​ ​
Sans", sans-serif;
​background: ​ var(--dark-grey);
​ ​
​color:​ var(--white);
​ ​
}
 
Then we will go into those media queries and add the appropriate background and text color: 
 
@media​ (prefers-color-scheme:
​ ​ dark)
​ ​ {
​body​ {
​background: ​ var(--dark-grey);
​ ​
​color: ​ var(--white);
​ ​
}
}

53
"Sass: Syntactically Awesome Style Sheets." https://sass-lang.com/.
​ ​ Accessed 2 Nov. 2020. 

106 
~ llh\.!,/lindsey
1Jl
wit

@media​ (prefers-color-scheme:
​ ​ light)
​ ​ {
​body​ {
​background: ​ var(--white);
​ ​
​color:​ var(--dark-grey);
​ ​
}
}
 
Now that we have those data attributes, we can start using those attributes to override the 
settings. First, if there are no operating system settings: 
 
body​ {
​font-family: ​ "Open​ Sans", sans-serif;​
​background: ​ var(--dark-grey);
​ ​
​color:​ var(--white);
​ ​

​ ​
&[ ​data-color-scheme="dark"]
​ {
​background:
​ var(--dark-grey);
​ ​
​color:
​ var(--white);
​ ​
}

​ ​
&[ ​data-color-scheme="light"]
​ {
​background:
​ var(--white);
​ ​
​color:
​ var(--dark-grey);
​ ​
}
}
 
Then we want to override the system defaults if they choose something other than those defaults: 
 
@media​ (prefers-color-scheme:
​ ​ dark)
​ ​ {
​body​ {
​background:​ var(--dark-grey);
​ ​
​color:​ var(--white);
​ ​

​ ​
&[ ​data-color-scheme="dark"]
​ {
​background:
​ var(--dark-grey);
​ ​
​color:
​ var(--white);
​ ​
}

​ ​
&[ ​data-color-scheme="light"]
​ {
​background:
​ var(--white);
​ ​
​color:
​ var(--dark-grey);
​ ​
}

107 
~ llh\.!,/lindsey
1Jl
wit

}
}

@media​ (prefers-color-scheme:
​ ​ light)
​ ​ {
​body​ {
​background:​ var(--white);
​ ​
​color:​ var(--dark-grey);
​ ​

​ ​
&[ ​data-color-scheme="light"]
​ {
​background:
​ var(--white);
​ ​
​color:
​ var(--dark-grey);
​ ​
}

​ ​
&[ ​data-color-scheme="dark"]
​ {
​background:
​ var(--dark-grey);
​ ​
​color:
​ var(--white);
​ ​
}
}
}
 
Now we want to test the following combos: 
1. System Defaults - select that radio button and then toggle the light and dark mode on your 
operating system 
2. Light mode - test that the radio buttons for the custom user settings are respected. 
3. Dark mode - test that the radio buttons for the custom user settings are respected. 
 
Now that we have that working, we want to remember this setting in localStorage.
​ ​ That way, 
when our user comes back to the website, the browser remembers their settings! 
 
At the beginning of writing this JavaScript, we created a storageKey
​ ​ and
​ used it to get the key 
from ​localStorage.​ Now we want to add that to the ​toggleColorScheme​ function: 
 
function​ toggleColorScheme()
​ ​ {
​ ​
​colorScheme​ = this.getAttribute("id");
​ ​ ​ ​  
localStorage. ​setItem(storageKey,
​ ​ ​ colorScheme);
​ ​
document. ​body.dataset.colorScheme
​ ​ ​ ​ ​  
​ = colorScheme;


 
Now when you refresh the page, the browser remembers your settings. You can also observe this 
by going to your dev tools, then storage, and select localStorage. You can then see the key change 
as you select other options. 
 

108 
~ llh\.!,/lindsey
wit1Jl
Final code: bit.ly/codepen-user-preferences
​  

   

109 
~ llh\.!,/lindsey
wit1Jl

Part 4 - Testing 
   

110 
~ llh\.!,/lindsey
1Jl
wit

In my opinion, testing is the most important consideration for creating accessible applications. For 
one, it's how a lot of people (myself included) get introduced to accessibility. For another, we are 
humans who grew up in an ableist system. No matter how good our intentions are, we are bound to 
make mistakes. Even for features not related to accessibility, we make mistakes. How many times 
have we introduced a new bug on production? Tests create a contract between what the expected 
accessibility features are. 
 
Even though I consider testing the most important, I wanted to talk about testing last. I know the 
Test Driven Development folks are probably screaming reading this, but hear me out. Most people 
learn about accessibility testing first because they didn't build their apps with inclusion in mind. 
They are backtracking through all the errors and fixing them. At almost every job I've had, testing 
has been in the form of an audit. Everyone is reacting by fixing the bug tickets. While I'm glad that 
we were addressing accessibility, I find that this makes accessibility way more stressful. 
 
My approach to testing has always been proactive. It includes understanding the accessibility 
fundamentals and using them to shape your method. 
 
There are three main categories of testing: Automated testing, Manual testing, and User testing. 

   

111 
~ llh\.!,/lindsey
1Jl
wit

Chapter 9 - Automated Testing 


I love automated testing! While it doesn't catch everything, it's helpful to add tests, so you're not 
introducing errors. If you're new to accessibility, testing will be crucial while you're building up 
your accessibility muscles. 
 
When I say automated testing, I am talking about two types of testing in particular. First is fully 
automated, such as linters and GUIs. Linters and GUIs can catch some of the errors that we talked 
about in Chapter 2. Second are unit, integration, and end-to-end (E2E) tests that you write. 

Linters 
The only linter I use for accessibility is eslint-plugin-jsx-a11y (bit.ly/eslint-plugin-jsx-a11y).
​ ​ I find 
this plugin is handy if you are using both eslint and JSX. It's pretty simple to configure if you 
already have eslint settings on your projects. Usually, it's configured in an .eslintrc file or 
package.json file under "eslintConfig": 
 
If you already have eslint installed, you can enable this plugin by 
 
1. Installing it using 
a. npm: npm​ install eslint-plugin-jsx-a11y --save-dev
b. yarn: ​yarn add eslint-plugin-jsx-a11y --dev
2. Add the plugin to the configuration object to your eslint configuration 
 
{
​"plugins": ​ [
​"jsx-a11y"
]
}
 
3. Add the extension of the rules  
 
{
​"extends": ​ [
​"plugin:jsx-a11y/recommended"
]
}
 
When you have this plugin enabled, it will take a look at your code and spot problems with how 
you're writing it. Be aware that this doesn't take into account CSS or how the final JSX renders on 
the page. So you may have to configure rules, and if you're newer to testing, it could get confusing. 
 

112 
~ 1Jl
llh\.!,/lindsey
wit

To see how it works, let's say we are returning this radio button: 
 
<div>
​ ​
<input ​ ​id="dark" ​ ​
​ type="radio"
​ ​ ​
​ name="theme"
​ ​ ​/>
<label> ​Dark</label>

</div>
 
In our Code editor (I use Visual Studio Code), I will have a red squiggly line underneath the code. 
That means it's causing trouble. 
 
<div>
<input id:-c"dark" type:-: radio"0 namec:-p'theme" />
<label>Dark</label>
</div>
 
 
If we hover over it because we haven't associated our label with our input, this is what we see. 
 
(property) JSX.IntrinsicElements.label: React.DetailedHTMLProps<React.LabelHTMLAttributes
<HTMLLabelElement>, HTMLLabelElement>
A form label must be associated with a control. esl1nt(jsx-ally/label-has-associated­
control)

 
 
Also, with how I configured it, my build straight up fails. 
 

 
 
Once I add in the ​htmlFor*,
​ the error goes away, and my code compiles: 
 

113 
~ llh\.!,/lindsey
1Jl
wit

<div>
<input id:-:1'dark 11 type:-: 11 radio" name::-11 theme 11 />
<l.abe l html. Fo r::::-
11 da rk">Dark</ label>

</div>
 
 
*in JSX, we say htmlFor
​ ​ instead of for
​ ​ because for
​ ​ is a JavaScript keyword. 

GUIs 
I find GUIs to be the most insightful because they point out where your error is. Most of the time, it 
gives you insight into why it's an issue and how to fix it. My favorite accessibility GUIs are: 
 
● Accessibility Insights (Microsoft)54 
● The WAVE tool (WebAIM)55 
● Lighthouse (Chrome DevTools)56 
 
At the time of this writing (October 2020), most of these tools give almost the same results. They 
have different formats of telling you the errors, and you can choose what works best for you.  
 
Let's test the User Preference menu we created in the last chapter. If we remove the for ​ ​ attribute 
from a label, you'll get the same number of errors in every one of these tools.  
 
You can use this failing toggle example to run these tests on your own: 
bit.ly/codepen-failing-toggle 
 
To use Accessibility Insights:  
 
1. Install the Accessibility Insights extension on Google
​ Chrome​ or ​Microsoft Edge 
2. Go to the website you want to test 
3. Select Fast Pass to get started quickly (they also have a handy introduction video 
bit.ly/youtube-a11y-insights)​  
4. This will open up a new window and will identify which errors are which 
5. If you have your element inspector open in the browser, you can click on the visual 
indicator. It will tell you what the error is. If you click on "inspect element," it will take you 
to the error that needs fixing in the developer tools. 
 
This is a brief screenshot tour of the Accessibility Insights GUI: 

54
​"Accessibility Insights." ​https://accessibilityinsights.io/.​ Accessed 2 Nov. 2020. 
55
​"WAVE Web Accessibility Evaluation Tool ...." ​https://wave.webaim.org/.​ Accessed 2 Nov. 2020. 
56

"Lighthouse | Tools for Web Developers ...." ​https://developers.google.com/web/tools/lighthouse.​ Accessed 
2 Nov. 2020. 

114 
Ipl FastPass V Target page: CodePen • color scheme a11y toggle, fails automated tests

I• Automated checks
Automated checks
Tab stops Automated checks can detect some common accessibility problems such as missing or
2
invalid properties. But most accessibility problems can only be discovered through
manual testing. The best way to evaluate web accessibility compliance is to complete an
assessment.

Failed instances a::D


) Expand all Visual helper a:)

C[D label: Ensures every form element has a label

 
Failed instances QD

v Collapseall Visual helper a:)

v GI) label: Ensuresevery form element has a label

Resourcesfor this rule

More information about label

WCAG 1.3.1, WCAG 4.1.2

Path #system-default

Snippet <input id="system-default" type=" radio" name="theme">

How to fix Fix ONEof the following:


• aria-label attribute does not exist or is empty
aria-labelledby attribute does not exist, referenceselements that do not exist or referenceselements that are empty
Form element does not have an implicit (wrapped) <label>
• Form element does not have an explicit <label>
• Element has no title attribute or the title attribute is empty
• Element's default semantics were not overridden with role= "none·
• Element's default semantics were not overridden with role="presentation'"

<0> Highlight visible

115 
Theme T

Ensures every form element has a label X


Rule name
label

Success criteria

Path
#system-defa

I ['.; Inspect HTML 11 [b Copy failure details 11 e File issue

Fix ONE of the following:


• aria-label attribute does not exist or is empty
• aria-labelledby attribute does not exist, references elements that do not
exist or references elements that are empty
• Form element does not have an implicit (wrapped) <label>
• Form element does not have an explicit <label>
• Element has no title attribute or the title attribute is empty
• Element's default semantics were not overridden with role="none"
• Element's default semantics were not overridden with
role= "presentation"

116 
~ llh\.!,/lindsey
1Jl
wit

",.d1·, id="user-preterences">
► -.butt,:,1·, class="button" id="color-scheme"•---/butt,:,n.,
.,. .:.tie ldse:.- ..
-. iegend class="sr-on ly">Theme</ l_e,~end.-
..,. <.rJl',./>

<inpJt id="system-default" type="radio" name~"theme"> $0


<label.>Syste~ Default</label>
</d1·,,.,
► ,.d1·,.•,---/div--
► --dn,·--,---/di·,,_,

</tie to set.,
 
 
I love the Accessibility Insights tool because it also has a tab order checker. This feature is helpful 
to get a sense of what elements are focusable and what is not. 
 

f- ➔ C O i cdpnJo/llttlc,;;.opc0903,lcJeouq:qBi··J'l

~-3
.,,:;ystem
· ...ight
Default

~Jark

Lorem [psum dolor stt a met consectetur adrpiscing elit.


quam tempus convalHs sed sit amet diam. Anquam dict1
accumsan nibh ex, quis porta odio rhoncus et. Donec vi
Aliquam facilisis neque orci, vel faucibus nunc bibendu
Donec et nibh ac libero lacinia lacinla ut sit amet felis. P
Sed eu fermentu m m1.

 
 
 
To use the Wave Extension, simply install it on Chrome or Firefox. Then you just click on the 
extension. 
 
This is a brief screenshot tour of the WAVE GUI. 

117 
The following apply to ,he emire page:

~WAVE powered by
WebAIM
web accesslolllty evaluation 1001

Styles: OFF

Summary
~
Summary Details Reference Structure Contrast

£13 o• 0
Errors Contrast Errors

A2 e2
Alerts Features

n.0 0
Structural Elements ARIA
 
 
 
You also get a page of errors and potential violations (alerts) 

118 
I I IE IOIIOWII1gapp19lb d IE El llir e page.

(IJWAVE powered by
WebAIM
web accesslblhty evaluar,on 1001

Styles: OFF

Details

~ El 3 Errors
~3 X Missing form label
aaa•


~ A2Alerts
~ 1 X No heading structure

~ 1 X No page regions
cjO

 
 
What I love about the Wave tool is how they go in-depth about WHY something is important. 
 

0
Summary Details Reference Structure Contrast

a Errors
Missing form label

What It Means
A form control does not have a corresponding
label.

Why It Matters
If a form control does not have a properly
associated text label, the function or purpose of
that form control may not be presented to screen
reader users. Form labels also provide visible
descriptions and larger clickable targets for form
controls.

How to Fix It
If a text label for a form control is visible, use the
<label> element to associate it with its respective
form control. If there is no visible label, either
provide an associated label, add a descriptive title
amibute to the form control, or reference the
label(s) using a ria-labelledby. Labels are not
required for image, submit, reset, button, or

 
hidden form controls.

119 
~ llh\.!,/lindsey
1Jl
wit

For Lighthouse you simply open up Chrome Developer Tools, and click on generate report. 

 
I use Chrome Lighthouse as a final gut check. 

Unit testing 
Unit tests are great for testing accessibility issues at a modular level. They're also great for when 
we build custom interactions inside of components. It's a great way to make sure those accessible 
interactions stick around. Any of the examples we created in Part 3 could be a good candidate for 
writing a custom test. 

120 
~ llh\.!,/lindsey
1Jl
wit

 
For this example, I am going to use Tabs. Most of the time you write tests, you're using a front end 
framework. I found a cool article on medium about writing unit tests without a framework, and I 
used that to write tests.57 
 
Before we start getting into writing tests, if you have never written tests before, that's okay! I'll do 
my best to explain what things mean as I go along. 
 
I use the jest58 and Testing Library framework59 to write my tests. Testing library has support for 
many front end frameworks, but it also has support for the DOM. 
 
So first things first, let's get set up. 
1. Go to your command line and clone this repo using git bit.ly/github-a11y-testing-demos.
​ ​ 
2. Once you do that in the same terminal, go into the unit-test-example directory. cd ​
book-testing-demos/unit-test-example/
3. Make sure you have at least Node 10 and run npm ​ install.​   
4. Once you've installed the project, you can run npm ​ start,​ and you will see the very 
unstyled but functional tabs we created.  
 
We already have a test file created. If we open this up in our code editor, you should see an src
​  
directory. Go in there and open up index.test.js.
​ ​ 
 
You'll see some of the setups that I got from that blog post. Then you'll see a keyword that says 
describe.​ This word means we are describing the test suite we're about to run. It takes a string 
and then a callback. The callback is where we will start writing our tests. 
 
The ​beforeEach​ callback runs before every test we create. In this instance, we are using the 
JSDOM constructor to generate HTML from our index.html file. Then we create a container that's 
the document body. The goal of JSDOM is "to emulate enough of a subset of a web browser to be 
useful for testing and scraping real-world web applications."60  
 
Now that we have the scaffold of our test setup let's start writing tests! What are a few things we 
can test? 
 
● Make sure that the arrow keys focus on the tabs 
● Make sure that the tabs activate the tab panels appropriately when we click on a new tab 
 

57
"How to Unit Test HTML and Vanilla JavaScript Without a UI ...." 23 Apr. 2020, 
https://levelup.gitconnected.com/how-to-unit-test-html-and-vanilla-javascript-without-a-ui-framework-c4c8
9c9f5e56.​ Accessed 2 Nov. 2020. 
58
"Jest · Delightful JavaScript Testing." https://jestjs.io/.
​ ​ Accessed 2 Nov. 2020. 
59
"Testing Library." https://testing-library.com/.
​ ​ Accessed 2 Nov. 2020. 
60
"jsdom/jsdom: A JavaScript implementation ...." https://github.com/jsdom/jsdom.
​ ​ Accessed 31 Oct. 2020. 

121 
~ llh\.!,/lindsey
1Jl
wit

For each bullet point, we can write a test. 


 
​ ​
describe("index.html", ​ () => {
// beforeEach code
​ ​
​it("Uses the arrow keys to focus on other tabs", ​ () => {
// Write first test here
});
});
 
So what can we put in here? Let's pause and talk about some of the tools we can use. 
● expect​ is a keyword from jest. You can wrap it around what you're testing and use 
matchers to tell your test what to expect (heh). If it doesn't match that, the test will fail.61 
● fireEvent​ is from Testing Library APIs. You can fire an event and then test to make sure 
you get what you expect.62  
● jest-dom extends the existing matchers from expect so you can test more in-depth.63  
 
So let's use some of those APIs to write our first test. First, let's grab the first tab and press the 
right arrow and see if the focus is where we expect it to be. 
 
​ ​
it("Uses the arrow keys to focus on other tabs", ​ () => {
​const​ ​firstTab​ = container.querySelector("#nils");
​ ​ ​ ​ ​ ​
fireEvent. ​keyDown(firstTab, ​ ​ ​ { key:
​ ​ "ArrowRight"
​ ​ });
});
 
We have only pressed the right arrow key once. What are some of the things we expect? We expect 
the first tab to no longer have focus. We also expect the tabindex of that first tab to be -1. 
 
​ ​
it("Uses the arrow keys to focus on other tabs", ​ () => {
​const​ ​firstTab​ = container.querySelector("#nils");
​ ​ ​ ​ ​ ​
fireEvent. ​keyDown(firstTab, ​ ​ ​ { key:
​ ​ "ArrowRight"
​ ​ });

expect( ​firstTab).not.toHaveFocus();
​ ​ ​ ​ ​
expect( ​firstTab).toHaveAttribute("tabindex",
​ ​ ​ ​ ​ "-1");
​ ​
});
 
You'd also expect the second tab NOT to have a tabindex attribute and to have focus. So we can do 
the opposite of that here. 
 

61
"Expect · Jest." https://jestjs.io/docs/en/expect.
​ ​ Accessed 2 Nov. 2020. 
62
"Firing Events · Testing Library." https://testing-library.com/docs/dom-testing-library/api-events.
​ ​ Accessed 
2 Nov. 2020. 
63
"testing-library/jest-dom - GitHub." https://github.com/testing-library/jest-dom.
​ ​ Accessed 2 Nov. 2020. 

122 
~ llh\.!,/lindsey
1Jl
wit

​ ​
it("Uses the arrow keys to focus on other tabs", ​ () => {
​const​ ​firstTab​ = container.querySelector("#nils");
​ ​ ​ ​ ​ ​
​ ​ ​
fireEvent.keyDown(firstTab, ​ { key:
​ ​ "ArrowRight"
​ ​ });

​ ​ ​ ​ ​ ​
expect(firstTab).not.toHaveFocus();
​ ​ ​ ​ ​ ​ "-1");
expect(firstTab).toHaveAttribute("tabindex", ​ ​

​const​ ​secondTab​ = container.querySelector("#agnes");


​ ​ ​ ​ ​ ​
​ ​ ​
expect(secondTab).toHaveFocus(); ​
​ ​ ​ ​ ​
expect(secondTab).not.toHaveAttribute("tabindex"); ​ ​ ​
});
 
Also, I want to test if we are on the first or last tab, we are circling correctly with the arrows. So we 
can go backward 2 and see if we are on the third tab. 
 
​ ​
it("Uses the arrow keys to focus on other tabs", ​ () => {
​const​ ​firstTab​ = container.querySelector("#nils");
​ ​ ​ ​ ​ ​
​ ​ ​
fireEvent.keyDown(firstTab, ​ { key: ​ ​ "ArrowRight"
​ ​ });

​ ​ ​ ​ ​ ​
expect(firstTab).not.toHaveFocus();
​ ​ ​ ​ ​ ​ "-1");
expect(firstTab).toHaveAttribute("tabindex", ​ ​

​const​ ​secondTab​ = container.querySelector("#agnes");


​ ​ ​ ​ ​ ​
​ ​ ​
expect(secondTab).toHaveFocus(); ​
​ ​ ​ ​ ​ ​ ​
expect(secondTab).not.toHaveAttribute("tabindex"); ​


fireEvent.keyDown(secondTab, ​ ​ ​ { key:
​ ​ "ArrowLeft"
​ ​ });

fireEvent.keyDown(firstTab, ​ ​ ​ { key:
​ ​ "ArrowLeft"
​ ​ });
​const​ ​thirdTab​ = container.querySelector("#complex");
​ ​ ​ ​ ​ ​
​ ​ ​
expect(thirdTab).toHaveFocus(); ​
​ ​ ​ ​ ​
expect(thirdTab).not.toHaveAttribute("tabindex"); ​ ​ ​
​ ​ ​
expect(firstTab).toHaveAttribute("tabindex"); ​ ​ ​
​ ​ ​
expect(secondTab).toHaveAttribute("tabindex"); ​ ​ ​
});
 
Pretty cool stuff! If you want to see the other test I wrote, it's on the completed-exercises
​  
branch of this repository. 
 
One thing to note is to make sure that we also purposefully fail our tests when we write them. We 
need to be sure we aren't giving ourselves false positives. There have been times when I thought 
my test gave me confidence, but when I tried to fail the test, the test also gave me a pass. Be sure 
that your test is giving you more confidence, not a false positive. 

123 
~ llh\.!,/lindsey
1Jl
wit

 
Other resources: 
● axe-core - bit.ly/github-axe-core
​  
● jest-axe - bit.ly/github-jest-axe
​  

Integration/E2E testing  
I don’t differentiate between Integration testing and E2E testing. I know some people feel the 
opposite, but I use these phrases interchangeably. The test in the last section was particular to the 
component we built. A lot of times, accessibility isn’t only on a component level. We want to make 
sure that things like overall contrast on the entire application passes. Sometimes we switch focus 
between two components. If we created a custom solution for Client-Side Routing, we would want 
to test for that. I imagine you could also test to make sure that reduce-motion settings are 
respected. Integration and E2E tests are a great way to do that. 
 
I’m going to write a few implementation tests using Cypress and Cypress-axe. I’m going to do this 
to ensure that we don’t implement a low contrast on our dark color mode. I’m also going to test 
that user preferences override system defaults. Additionally, I want to test that the browser 
retrieves the settings from localStorage.  
 
To get started: 
1. Go to the cypress-example directory in the Github repository we talked about in the last 
section. From the root of the directory, type cd
​ cypress-example
2. Make sure you have at least Node 10 and run npm ​ install.​  
3. Once that’s done, you can run npm​ start.​  
 
Note: this is a ReactJS application that I made to recreate what we did in the User Preferences 
chapter loosely. I wouldn’t worry about the react application too much. 
 
To run cypress, we must have our app running simultaneously. So keep npm ​ start​ running and in 
a new terminal window run npm ​ run cypress:open.​ This command will trigger the cypress 
application to open. To start running the tests, press the “Run all specs” button. 
 

124 
• • • /Users/lkopacz/Sites/book-testing-demos/cypress-example

cypress-example O Support ~ Docs ~ Log In

</>Tests e Runs II) Settings • Chrome 86 ....

0. Search ... ► Run all specs

• INTEGRATION TESTS COLLAPSE ALL I EXPAND ALL

• e, examples
Cl actions.spec.js

Cl aliasing.spec.js

Cl assertions.spec.js

Cl connectors.spec.js

Cl cookies.spec.js

Cl cypress_api.spec.js

Cl files.spec.js

Cl local_storage.spec.js

Cl location.spec.js

Cl misc.spec.js
..
Version 5.5.0 Changelog
 
 
When we ran cypress:open, we created a new cypress folder in the root of this directory. We’ll be 
writing our tests in the integration folder. 
 
First, I want to test to make sure that the color contrast is passing. We can use cypress-axe to help 
us with that. After we first run cypress:open, we can go into our integration folder and start 
creating tests. Let’s delete the example directory since we don’t need any of those tests. 
 
In the integration folder, create a page.js file. 
 

v cypress
> fixtures
v integration
JS page.js
> plugins
> support
 
 

125 
~ llh\.!,/lindsey
1Jl
wit

In that file, we’ll use a similar language that we were using for our unit tests. The main difference 
is what will go inside our tests. 
 
​ ​
describe("page render", ​ () => {
​beforeEach(() ​ => {
// run code here
});

​ ​
​it("should ​ () => {
have proper contrast",
// run code here
});
});

We can use the .visit()


​ ​ command64 to visit a page. We’ll be using that in both the tests we write. 
We’ll also use the cypress-axe API to injectAxe before every test65.  
 
​ ​
describe("page render", ​ () => {

​beforeEach(() => {
cy. ​visit("http://localhost:3000/");
​ ​ ​
cy. ​injectAxe(); ​
});

​ ​
​it("should have proper contrast", ​ () => {
cy. ​checkA11y("body",
​ ​ ​ {
​runOnly:
​ ["cat.color"],
​ ​
});
});
});
 
When we run this, our test should pass because our color scheme has an appropriate contrast. 
  

64
"visit | Cypress Documentation - Why Cypress?." 28 Oct. 2020, 
https://docs.cypress.io/api/commands/visit.html.​ Accessed 2 Nov. 2020. 
65
"component-driven/cypress-axe: Test accessibility ... - GitHub." 
https://github.com/component-driven/cypress-axe.​ Accessed 2 Nov. 2020. 

126 
CDlocalhost:3000/_J#/tests/_all

Chrome is being controlled by automated test software. X

<Tests X -- 0 -- 02.15 • t C 0 http://locolhost:3000/ 1000 X 660 (56%) 0

All Specs

""' page render

._, should have proper contrast

• BEFOREEACH

visit http://localhost:3000/

,.. TEST BODY

-l!m!!D 0 accessi.bi. li ty violations were


detected: expected 0 to equal 0

 
 
But what if we purposefully fail to be sure. Let’s go into the App.css file. Right now, the --dark-grey 
variable is #363636 
 

:root ​ {
--white: ​#fff;​
--dark-grey: ​#363636; ​
}
 
What if we changed that --dark-grey
​ ​ variable to #b3a9a9 and re-ran the test? 

127 
Chrome is being controlled by automated test software. X

<Tests Xl 0-- 02.39 • l 0 http://localhost:3000/ 1000 X 660 (56%) 0

All Specs

visit http:/ /local host: 3000/

• TEST BODY

- ally error! color-contrast on 7 Nodes D


-rmm detected:
1 accessibi. l i ty violation
expected 1 to
was
equal 0

O AssertionError

1 accessibility violation was detected:


expected 1 to equal 0

node_modules/cypress-axe/dist/index.js:4:443703

2
3 }, {}], "Focm": [functi.on(require,module
> 4 "use strict";functi.on e(e,n){if(null=

-
5 },{"fs":"rDCW"}]},{},["Focm"], null)
6 / /# sourceMappingURL=lindex. js. map

View stack trace Q Print to console

 
 
This isn’t the most readable error, but before it turns red, you see ‘a11y error, color-contrast on 7 
nodes’. 
 
Now that we know that test is reliable, let’s move on to the next one. 
 
In the cypress/integration directory, let’s create a new file called theme-picker.js. In that file, we’ll 
create our test scaffold again. 
 
​ ​
describe("user color scheme preferences", ​ () => {

​beforeEach(() => {
​// Write before each
});

​ ​
​it("changes and maintains the color scheme on user preferences", ​ () => {
​ // start interacting
});
});
 
Now that we have that scaffolding up let’s add the cy.visit() command to the beforeEach callback. 
Then start writing our test. 
 

128 
First, we are going to use the .get() command66 to get the light radio button and click on it67. 
 
​ ​
describe("user color scheme preferences", ​ () => {

​beforeEach(() => {
cy. ​visit("http://localhost:3000/");
​ ​ ​
});

​ ​
​it("changes and maintains the color scheme on user preferences", ​ () => {
cy. ​get("#light").click();
​ ​ ​ ​ ​
});
});
 
The cool thing about this is that you can see what it's doing in real-time when you're writing the 
tests. 
 
<Tests X -- 0 -- 00.23 0 h ttp://locolhost:3000/ 1000 X 660 (60%) 0

All Specs ITheme TI


0 System Default
• user color scheme preferences (!)Light
ODark
.; changes and maintains the color scheme on user
preferences Lorem ipsum dolor sit amet, consectetur ad1p1scmg
elit Phaselluseu convallis ipsum. In manis lectus lectus, a mterdum dolor pona eu.
Suspeno1sse ultric1es ex quis quam tempus convallis sed sit amet diam. Ahquam dictum metus nee ele1fend pulvmar. In cursus diam
posuere, vestibulum ligula non. fermentum metus Nunc accumsan nibh ex, quis porta odio rhoncus et. Donec viverra et libero sit amet
"" BEFORE EACH
cgestas. Suspend1sse Ipsum ipsum, volutpat v,1ae vulputate sempcr, sempcr quis est Aliquam facilisis neque orci, vel fauc1bus nune
visit http://locolhost:3000/ bibendum vel. Praesent a urna at lea fae1hs1stempus eu sed tortor. Donee venenatIs b1bendum libero. Sed nee mollrs erat. Donee et
nibh ae libero lae,ma lae1ma ut sit amet fehs. Phasellus at diam faue1bus, plaeerat nisl quis. suseIp,t nsus, lorem Ipsum dolor sit amet.
eonseetetur ad1piscing e!tt. Sed eu ferment um mI
,.. TEST BODY
Praesent finibus augue sit amet dap1bus bland 11.Etiam vitae libero et od o ornare pulv1nar ege1 eget urna Proin porttItor digmss1m
get #light nsus sit amet fimbus. Vestibulum tineidunt egestas pharetra. Donee eu egestas mbh. Phasellus aecumsan laoreet rutrum. Phasellus
tempus nune a iorem fermenturn, id aliquet dui pulvinar. Vest1bulum lacinia tincidunt urna eu vehicula. Pellentesque viverra acus nis1
- click
Oonec fells mauns, vanus ae accumsan id, facihs1squIs Ipsum. Integer eu sap1en sit amet psum 1acuhs effiCllur ve! quis msi. Integer at
1gula nee mi ultrie,es pellentesque. Morb1 sit amet euismod augue

Proin d1gniss1mleetus nee pulvinar euismod Vestibu!um ut moll is fells, aceumsan elementum massa. Fusee a interdum quam, sed
ultriees ipsum Donee consequat ex vel eras lobortis tacinia. lorem ipsum dolor sit amet, consectetur adipiscing elit Duis nee
ullamearper nosl. Maecenas eu candimentum ante. Ahquam erat volutpat Sed laareet tineidunt fauc1bus. VNamus 11ne1duntlea
eammodo, sagittis lcctus eget. eonvaltis sem. Morb, eras turpis, eursus eu leetus nee, ultricies gravida urna. Cras turpis lorem, suseipit
nee consequat id, tristique s,t amet diam. Sed porta purus arcu, at rutrum nulla rutrum quis. In sag ttIs, lectus sed fermentum vehicula,
erat erat portlltor du1, ut venenatIs urna tellus ac od o. Suspendisse fring1lla venenat,s dui, semper pretIum turpIs, Cras eleifend, nunc
eu auctor faueibus, risus diam grav,da metus, sect commode ante mbh a mauns

 
 
Even though we can see that what we expect is happening in the interface, we need to write it in 
our test using the should command68 and CSS assertions69. 
 
​ ​
describe("user color scheme preferences", ​ () => {
​beforeEach(()​ => {

66
"cy.get() in the .within() command - Why Cypress?." 28 Oct. 2020, 
https://docs.cypress.io/api/commands/get.html.​ Accessed 2 Nov. 2020. 
67
"click | Cypress Documentation - Why ...." 28 Oct. 2020, https://docs.cypress.io/api/commands/click.html.
​ ​ 
Accessed 2 Nov. 2020. 
68
"should | Cypress Documentation - Why Cypress?." 28 Oct. 2020, 
https://docs.cypress.io/api/commands/should.html.​ Accessed 2 Nov. 2020. 
69
​"Assertions | Cypress Documentation - Why Cypress?." 28 Oct. 2020, 
https://docs.cypress.io/guides/references/assertions.html.​ Accessed 2 Nov. 2020. 

129 
cy. ​visit("http://localhost:3000/");
​ ​ ​
});

​ ​
​it("changes ​ () => {
and maintains the color scheme on user preferences",
cy. ​get("#light")
​ ​ ​
. ​click() ​
. ​get("body")
​ ​ ​
. ​should("have.css",
​ ​ ​ "background-color",
​ ​ "rgb(255,
​ ​
255, 255)");
});
});
 
As we expected, this test passes! 
 
<Tests X -- 0 -- 00.23 C 0 http://localhost:3000/ 1000 X 660 (60%) 0

All Specs ITheme TI


0 System D€fault
• user color scheme preferences (!)ight
0Dark
-./ changes and maintains the color scheme on user
preferences Lorem 1psumdolor sit amet. consectetur ad1pisc1ng
el,t. Phaselluseu conva1·1s
1psum.In maws lectus lectus, a interdum dolor porta eu
Suspendisse ultrioes ex quis quam tempus convallis sed sit amet diam. A:,quam dictum metus nee ele1fend pulvinar. In cursus dram
posuere,vest1bulum11gula non, ferrnemum metus.Nunc accumsannibh ex, qu1sporta od10rhoncuset. Donecviverraet libero sn amet
• BEFORE EACH
egestas. Suspend1sse ipsum 1psum, volutpat vitae vulputate semper, semper quis est. Aliquam facilisis neque orci, vel faucibus nunc
visit http://localhost:3000/ b1bcndum vel. Pracscm a urncl at leo faeil,s1stcmpus eu sed tortor. Donec venenatis bibendum libero. Sed nee mollis erat Donec et
nibh ae libero lae,nia lacinia ut sit amet felis. Phasellus at diam faueibus, plaeerat nisl quis. suseIpIt risus. Lorem ipsum dolor sit amet,
eonseetetur adipiseing elit. Sed eu fermentum mi
,.. TEST BODY
Praesent finibus augue sit amet dapibus blandil. Etiam vitae libero et odio ornare putv,nar eget eget urna Proin porttitor dignissim
get #light nsus sit amet finibus. Vestibu!um tincidunt egestas pharetra Donee eu egestas nibh. Phasellus accumsan laoreet rutrum. Phasellus
tempus nune a lorem fermemum, id ahquet du1 puMnar. Vest1bulum laein1a t1ne1dunt urna eu '1€'hieula Pellentesque viverra lacus nisi
- click
Donee fehs mauns. varius ae aeeumsan id. fac1lisis quis 1psum. Integer eu sapien sit amet ,psum ,aculis efficiturvel quis nis1. Integer at
get body l1gula nee m1 ultricies pellentesque. Morb1 sit amet eu1smod augue

-llmDI expected <body> to have CSS Proin d gnissim leetus nee pul\llnar euismod. Vestibulum ut molhs felis, accumsan e ementum massa. Fusee a interdum quam, sed
ultriees 1psum. Donee consequat ex vel eros lobort1s lae1rna.Lorem ,psum dolor s11amet. consectetur ad1p1seingel1t Duis nee
property background-color with the ullameorper nisl. Maecenas eu condimentum ante. Aliquam erat volutpat Sed laoreet tineidunt faueibus. Vivamus t1ne1dunt leo
value rgb(255, 255, 255) eommodo, sag1t11sleetus eget, eonvalhs sem. Morbi eros turp1s, eursus eu leetus nee. u1tne1esgrav1da urna eras turpis lorem, suse1p1t
nee consequat id, tris1ique sit amet a1am. Sed pona purus arcu. at rutrum nulla rutrum quis. In sag1tt1s.lectus sed fermentum vehicula,
erat erat pom1tor du1, ut venenat1s urna cellus ae odio. Suspend1sse fnng1lla venenmIs du1, semper pretium turpis. eras ele1fend. nunc
eu auctor faucibus. risus diam gravida metus, sed commodo ante nibh a mauns

 
 
Then we can use reload()70 to see if that same color scheme is working (meaning that localStorage 
is doing its thing) 
 
​ ​
describe("user color scheme preferences", ​ () => {
​beforeEach(()​ => {
cy. ​visit("http://localhost:3000/");
​ ​ ​
});

​ ​
​it("changes ​ () => {
and maintains the color scheme on user preferences",
cy. ​get("#light")
​ ​ ​
. ​click() ​
. ​get("body")
​ ​ ​

70
"reload - Why Cypress?." 28 Oct. 2020, https://docs.cypress.io/api/commands/reload.html.
​ ​ Accessed 2 Nov. 
2020. 

130 
. ​should("have.css",
​ ​ ​ "background-color",
​ ​ "rgb(255,
​ ​
255, 255)");

.reload() ​
​ ​ ​
.get("body") ​
. ​should("have.css",
​ ​ ​ "background-color",
​ ​ "rgb(255,
​ 255, 255)");​
});
});
 
<Tests X- 0-- 00.34 0 http://localhost:3000/ 1000 X 660 (60%) C,

All Specs I Theme...-1


OSystemDefault
• user color scheme preferences OL,ght
0Dark
v' changes and maintains the color scheme on user
preferences Lorem 1psumdolor s1lamet. consectewr ad1p1scing
eht Phaselluseu convallisipsum. In mattts lectus lectus,a interdurn dolor pona eu
Suspend1sse ultricies ex qu1squam tempus convallts sed sit amet diam. Aliquam dictum metus nee ele1fend pulVinar. In cursus diam
posuere, vest1bulumhgula non, fermentum metus. Nunc accumsan nibh ex, quIs porta od10rhoncus et Donec VNerra et libero sit amet
• BEFOREEACH
egestas. Suspend1sseIpsum ipsum, volutpat vitae vu1putatesemper, semper quIs est Ahquam faolisis neque orcI, vel fauc1busnunc
visit http://localhost:3000/ b1bendumvel. Praesent a urna at leo facilisis tempus eu sed tortor. Donec venenat1sb bendum libero. Sed nee molhs erat. Donec et
nibh ac libero lacin1alacinia ut sit amet fells Phasellusat diam fauc1bus,placerat nisl qu,s, suscipIt risus. Lorem tpsum dolor sit amet,
consectetur adipiscing elit Sed eu fermentum mi
• TEST BODY
Praesent fin,bus augue sit amet dap1busbland1t.Et1amvitae libero et od10ornare pulvinar eget eget urna. Proin portt,tor d1gn,ss1m
get #light nsus sit amet finibus. Yest1bulumt1ncidunt egestas pharetra. Donec eu egestas nibh. Phasellusaccumsan laoreet rutrum Phasellus
tempus nunc a lorem fermentum. 1daliquet dui pulvinar. Vestibulum lacinia tincidunt urna eu vehicula. Pellentesque viverra lacus nisi
- cl i.ck
Donec fel1smauris, varius ac accumsan 1d,fac1hs1s qu,s Ipsum. Integer eu sapIen sit amet ipsum iacuhs effic1turvel quis nis1.Integer at
get body ligula nee mi ultrieies pellentesque. Morbi sit amet euismod augue

-l'llll!lD expected <body> to have CSS Pro,n digniss1mleetus nee pulvinar euismod Vest1bulumut mollis fel1s,aceumsan elementum massa Fuseea ,merdum quam, sed
ultrices ipsum. Oonec consequat ex vel eros Jobon:islacinia. Lorem ipsum dolor sit amet, consec1e1uradipiscing elit. Duis nee
property background-color with the ullamcorper nisl. Maeecnoseu eond,mcntum ante. Ahquam erat vo!utpat Sed laoreet t1ne1duntfaueibus. Vivamus tincidunt leo
value rgb(2S5, 255, 255) commodo, sag1ttislectus ege1,convalhssem. Morb1eros turpis, cursus eu lectus nee, ultnoes grav1daurna eras turpis lorem, suscIpit
nee consequat id, tnstIque sit amet diam Sed porta purus arcu, at rutrum nulla rutrum quis. In sagittis, lectus sed ferrnentum veh1cula,
reload erat era1porn,tor dui, ut venenatis urna tellus ac od o. Suspendissefring1llavenenatis dui, semper pretIum turp,s. eras ele1fend,nune
eu auctor fauobus, nsus diam gravida rnetus, sed commodo ante nibh a rnauns
get body
-l'llll!lD expected <body> to have CSS
property background-color with the
value rgb(255, 255, 255)

 
 
Let’s try to fail the test on purpose by removing the localStorage
​ ​ setting in the toggle. 
 

131 
<Tests Xl 0-- 04.30 •t C 0 ht tp://locolhost:3000/ 1000 X 660 (60%) 0

All Specs
propen:y oacKgrouna-cotor wren
the value rgb(255, 255, 255)
reload
6 get body
7 - mt!D! expected <body> to have CSS property
background-color with the value
rgb(ZSS, 255, 2S5) , but the value was
rgb(S4, 54, 54)

0 AssertionError

Ti.med out ret ryi. ng: expected '<bOdY>' to have


CSS property 'background-color' with the value
'rgb(255, 255, 255)', but the value was
'rgb(54, 54, 54)'

cypress/integration/theme-picker.js: 13:8

11 . reload()
12 .get("body")
> 13 . should("have. css", "backgrour
A

14 }) ;
15 }) ;
16

View stack trace E) Print to console

 
 
I hope that with these couple of examples, you can see how robust integration testing can be. 

   

132 
~ llh\.!,/lindsey
wit1Jl

Chapter 10 - Manual Testing & User Testing 


I’m combining these two testing types into one chapter because I see many parallels between the 
two. Even though they are different, because both rely on a user, not an automated test, there’s a 
ton to learn. I highly recommend reading through Eric Bailey’s article on the importance of 
manually testing. The fourth myth is critical. Even with all the accessibility standards we have, we 
tend to assume that our assistive technology users are super users. A lot of times, that’s not true. 
Not everyone knows how to use an assistive device most optimally. 
 
For example, a fellow web developer in our community, Josh W. Comeau, can no longer code using 
a keyboard. He wrote a blog post showing how he codes with an eye tracker and diction. But as this 
is a relatively recent development, he admits he’s still only at 50% of his normal speed.71  
 
We need to remember that our disabled users aren’t all magically trained to use all the assistive 
technology. Remember, disability often develops later in life. There’s a learning curve, and we want 
to do our best to guide our users through our application. 
 

Manual Testing 
As much as it’s helpful to include linters, continuous integration, and custom unit and integration 
tests, they can’t catch everything. It’s always important when auditing a website to do a little bit of 
manual testing. For any pull request to be approved at my current workplace, we have required 
manual accessibility testing that must be completed. They had this workflow before I joined the 
team, which was a welcome change to what I usually see (me pestering people to add accessibility 
testing to the workflow). 
 
Because this isn’t the norm, I wanted to share a manual testing process that you could borrow 
from. 

Keyboard Testing 
The lowest hanging fruit in terms of manual testing is keyboard testing. You should be using your 
tab key to try to get to all the interactive features at a minimum. Additionally, when we use 
interactive features, it’s common to use the arrow keys to navigate and the escape key to exit out 
of interactive elements. Are those working correctly? 
 
Does the flow of the tab order make sense? It should if we weren’t trying to using a tabindex 
greater than 0. But it’s always good to check and make sure. Sometimes even when we use 

71
"Hands-Free Coding - Josh Comeau." 21 Oct. 2020, 
https://joshwcomeau.com/accessibility/hands-free-coding/.​ Accessed 2 Nov. 2020. 

133 
~ llh\.!,/lindsey
1Jl
wit

tabindex correctly, we could do a better job of guiding users through the application. A lot of 
times, this is caught in keyboard testing. 
 
I like to take note of all the features that we develop for mouse users. Are we able to interact with 
all the controls that a mouse user can? Every time we press the tab key, can we see where the 
focus is? Often, we are focusing on items that we cannot see because they are positioned 
offscreen, ready to be animated in. We also want to be sure that we aren’t tabbing to unnecessary 
elements in general. 

Screen reader testing 


Screen reader testing is always incredibly insightful because a lot of times, even when you pass all 
the automated accessibility tests and even keyboard tests, there’s still a thing or two that you 
realized you didn’t address.  
 
When I test with a screen reader, I do the following at a minimum: 
1. Make sure I can interact with the components that I need to 
2. Ensure that I can navigate through the application using headings, links, and line by line 
through the text. 
3. Make sure that the semantic elements like navigation, main, footer are communicated 
properly 
4. That nothing is hidden from a screen reader that should be exposed 
5. On the opposite end, make sure that we aren’t being announced items that are redundant 
or don’t make sense 
 
You should always be testing with at minimum one screen reader, but ideally more. There’s a lot of 
varying opinions about what combinations of screen readers and browsers work best together, but 
what I’ve found are these combinations are the most representative: 
 
1. JAWS with IE and Edge 
2. NVDA with Firefox and Chrome 
3. VoiceOver on Safari (both Mac and iOS) 
4. Talkback on Chrome (Android) 
 
I suggest that you survey your users to determine which screen readers and browsers you’ll use 
most in your testing methodology. Before you start testing, it’s beneficial to get a sense of all the 
commands you can use with each. 
● VoiceOver bit.ly/webaim-voiceover
​  
● JAWS bit.ly/webaim-jaws
​  
● NVDA bit.ly/webaim-nvda
​  
 
A few notes: 

134 
~ llh\.!,/lindsey
1Jl
wit

● If you are a mac user, I’ve had success using a Windows virtual machine and testing JAWS 
and NVDA on that. bit.ly/windows-vms
​  
● JAWS is a paid software, but if you are using it for testing, JAWS has a 40-minute mode for 
free. Once you restart your computer (or if you’re a Mac user, your VM), you can restart your 
40-minute mode. 

Other manual testing 

There are a few other miscellaneous manual tests I do 


● Turning your screen magnification way up (300%+) using Command/Control and the Plus 
key. 
● Verifying alternatives for audio elements (transcripts). 
● Verifying accessibility media query selectors work well (reduce motion, high contrast mode) 
● Walking through an accessibility audit using Accessibility Insights. 

User Testing 
As mentioned at the beginning of this chapter, the most crucial part about User Testing is 
understanding that not all your users are super users. You can always abide by standards, but if 
those standards aren't the best user experience, they are moot. I'm not an expert in conducting 
user research, but you should still advocate for it even if you're not an expert. 
 
Many companies help coordinate user testing, and some larger companies have UX research 
departments. But regardless of how you conduct user research, it's an essential part of the process. 
There are accessibility issues that can easily be missed in automated testing and manual testing 
based on our biases. 
 
Sometimes it's essential to get a sense of a specific area you want to test to narrow down test 
samples. Other times, you may want to put an app in front of disabled users and see what feedback 
they have. Like any experiment, we want to be sure we are using a diverse set of participants. All 
types of users will notice different things. 
 
For example, I read a case study about how a deaf user gave a delivery app feedback that the 
company was terrible for deaf people, even with an accessibility department.72. Having a phone 
number as the only way to contact a company is not something you wouldn't flag on an automated 
test and wouldn't necessarily be something you catch manually without user feedback. But in this 
example, we see that even the accessibility lead missed a major flaw in their application, which 
was requiring a phone number to make orders; otherwise, you'd be seen as a phony. 
 

72
"Accessibility user testing: a cautionary tale | by Daniel Pidcock ...." 
https://uxdesign.cc/disabled-user-testing-a-cautionary-tale-b6cf64425adb.​ Accessed 2 Nov. 2020. 

135 
~ llh\.!,/lindsey
1Jl
wit

Then there was the example of Aaptiv, an audio-only fitness app that was popular among blind 
users. They implemented a big update that made it impossible to navigate to workouts with 
VoiceOver.73 If
​ they had done some user testing or consulted more with their users beforehand, 
they could have kept their blind users happy. 
 
Other resources: 
● W3C - Involving Users in Evaluating Web Accessibility bit.ly/w3c-involving-users
​  

   

73
"Lessons in iOS Voiceover Accessibility | Aaptiv Engineering." 26 Jun. 2018, 
https://medium.com/aaptiv-engineering/lessons-in-ios-voiceover-accessibility-834c5ed9a374.​ Accessed 2 
Nov. 2020. 

136 
~ llh\.!,/lindsey
1Jl
wit

Conclusion 
This book has been a labor of love that I’ve wanted to write for well over a year. Thank you so 
much for reading through this and I really hope that you’ve learned some practical knowledge that 
you can apply to your future applications and websites. I am human and make mistakes, if there are 
any errors in this book that you notice, grammatical or code, please email me at 
hello@a11ywithlindsey.com​ starting with the subject line “Book Typo -” Or “Book Code Typo -” 

137 

You might also like

pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy