The Bootcampers Guide To Web Accessibility
The Bootcampers Guide To Web Accessibility
Preface 4
Authors Notes 5
Who is this book for 6
Why I am writing this book 7
Goals 8
Thank you 8
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
2
~ llh\.!,/lindsey
wit1Jl
Chapter 8 - User Preferences 100
Conclusion 136
3
~ llh\.!,/lindsey
wit1Jl
Preface
4
~ 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.
5
~ llh\.!,/lindsey
1Jl
wit
6
~ llh\.!,/lindsey
1Jl
wit
7
~ 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
8
~ llh\.!,/lindsey
wit1Jl
9
~ llh\.!,/lindsey
1Jl
wit
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
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
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
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
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.
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
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>
21
~ llh\.!,/lindsey
1Jl
wit
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.
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.
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.
25
~ llh\.!,/lindsey
1Jl
wit
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.
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
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
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
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.
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
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
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.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
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
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
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 ~
46
. -
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 :
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
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
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.
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
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;
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;
#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);
64
~ llh\.!,/lindsey
1Jl
wit
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);
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
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
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
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
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.
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
.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:
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
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
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
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
// 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
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>
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>
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
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 QD
Path #system-default
115
Theme T
Success criteria
Path
#system-defa
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',./>
</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
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
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",
expect(firstTab).not.toHaveFocus();
"-1");
expect(firstTab).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
• 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
});
});
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
All Specs
• BEFOREEACH
visit http://localhost:3000/
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
All Specs
• TEST BODY
O AssertionError
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
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
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
-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,
-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
cypress/integration/theme-picker.js: 13:8
11 . reload()
12 .get("body")
> 13 . should("have. css", "backgrour
A
14 }) ;
15 }) ;
16
I hope that with these couple of examples, you can see how robust integration testing can be.
132
~ llh\.!,/lindsey
wit1Jl
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.
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.
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