Web Accessibility Tips and Tricks

Alex Oxrud
WW Tech Blog
Published in
12 min readJul 16, 2021

--

At WW, the engineering team has been building accessible web experiences by consulting the WAI-ARIA specification and discovering patterns through trial and error. The following tips and tricks are how we manage to create accessible web pages that work with most modern assistive technologies.

Intended audience

The following document is intended for readers who have a programming background and are looking for quick solutions for common problems when building an accessible web experience. The document will show inaccessible examples and accessible examples with brief descriptions.

TLDR

  • Use semantic HTML elements whenever possible
  • Links and buttons should have unique labels
  • Provide additional context to screen readers
  • Prefer re-structuring HTML to maintain sensible focus order
  • Test the experience to ensure it works as designed

Think about different methods of navigating a web page

There are a number of different ways a user can navigate a page. Some common ones include:

  • A mouse
  • A touchscreen (e.g., smartphone, tablet)
  • Keyboard navigation
  • A screen reader

When building a feature, consider how it can be navigated through these mediums. For example, a common pattern is to use a mouseover state to reveal a menu. That sort of functionality works well for mouse users, but it’s almost inaccessible by a touchscreen, keyboard, and screen reader.

Screen readers

A screen reader is a technology that allows blind and visually impaired users to read the content that is displayed on a web page. Nearly all computers and mobile devices have a screen reader built-in. The most popular screen readers are JAWS, VoiceOver, and NVDA

Semantic HTML

Semantic HTML is the foundation of accessibility in a web application. Using these various HTML elements to reinforce the meaning of information on our websites will often give us accessibility for free.

Lists

Any time there is content that would benefit from announcing how many items are included, use a list instead of a <div>

<!-- bad -->
<div>
<div>Fruits</div>
<div>Vegetables</div>
</div>

A screen reader would announce the above as “Fruits. Vegetables.”

<!-- good -->
<ul>
<li>Fruits</li>
<li>Vegetables</li>
</ul>

A screen reader would announce this as “List. Two items. Fruits, one of two. Vegetables, two of two. End of list.”

Links and buttons

Non-interactive elements, like a div with a click listener, are not recognized by screen readers and will be skipped.

<!-- bad -->
<div onclick="handleClick()">
learn more about food
</div>

A screen reader will announce this as “Learn more about food” and it will not state that it is an interactive element.

Instead of using those non-interactive elements, use links and buttons.

If there is a URL, use <a href=”url”>.

<!-- good -->
<a href="https://example.com">learn more about food</a>

A screen reader would announce this as “Link. Learn more about food.”

If there is no URL and clicking will trigger some action on a page, use <button>.

<!-- good -->
<button onclick="handleClick()">learn more about food</button>

A screen reader would announce this as “Learn more about food. Button.”

Landmarks

Using the correct landmark elements helps assistive technologies navigate the page and locate content more effectively. (See the full list of available landmarks in HTML5.)

<!-- bad -->
<div class="main-content">My content</div>

Instead of <div> for the main content of the page, you could use <main>.

<!-- good -->
<main class="main-content">My content</main>

A screen reader would announce this as “Main. My content. End of main.”

Navigation landmark

The navigation landmark allows screen readers to quickly locate all the main navigations on the website. It is intended for major blocks of navigations.

<!-- bad -->
<div class="navigation">
<a href="https://example.com">Example</a>
</div>

A screen reader would announce the above as “Link. Example.”

<!-- good -->
<nav class="navigation">
<a href="https://example.com">Example</a>
</nav>

A screen reader would announce this as “Navigation. Link. Example. End of navigation.”

<!-- better -->
<nav class="navigation">
<ul aria-label="About our company">
<li><a href="https://example.com">Example</a></li>
</ul>
</nav>

Even better, consider using navigation and a list. A screen reader would announce the above as “Navigation. List. About our company. One item. Link. Example. End of list. End of navigation.”

Use the role attribute carefully

Most semantic HTML elements can be expressed as non-standard elements (e.g., <div>, <span>, etc.) with a specific role attribute. However, more often than not, screen readers don’t handle these “mocked” elements the same way. Which leads one to add additional functionality (e.g., keyboard shortcuts, announcements) to get the same behavior as a semantic element.

Rule of thumb: If there is a semantic HTML element for the purpose, use the semantic element instead unless backward compatibility with older browsers is a concern.

Additional context

There are many instances in modern user interface design where the content and styling around the call to action provide the context for the label in the call to action.

Some assistive technologies navigate a page by reciting content on the page, like all links on the page. A screen reader will not describe the styling surrounding the entity it is reading. This poses a problem when there are many links that have the same labels.

For example, consider a page where there are many links that say “Shop now” and nothing else. When a screen reader recites all the links on the page, the user won’t fully understand the context for the “Shop now” link. Providing additional context via one of the methods below can help.

A screenshot of the WW Shop page showing two sections with a call to action button that share the same label of “shop now”

CSS hidden text

It is possible to provide additional context to a label without making it visible on the page. It keeps the design tidy as designers intended and makes it more accessible to assistive technologies. For example:

<a href=”https://example.com">
View more
<span class=”visually-hidden”>products in Healthy Snacks</span>
</a>

A screen reader would announce this as “Link. View more products in Healthy Snacks.”

The visually-hidden CSS class moves the content out of view so visual users will not see it, but screen readers will. The corresponding visually-hidden CSS declaration is:

.visually-hidden {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}

aria-label

Another approach is the aria-label property, which allows us to override what the screen reader would normally announce. For example:

<a href=”/product/chocolate” aria-label=”Add chocolate to cart”>
Add to Cart
</a>

A screen reader would announce this as “Link. Add chocolate to cart.”

aria-describedby and aria-labelledby

These two attributes for providing additional context have inconsistent behavior among assistive technologies. Consider using aria-label or the hidden text approach demonstrated earlier.

The aria-describedby property allows us to declare multiple element ids that provide additional details to the element’s announcement.

<div id=”product-title”>Chocolate</div><a href=”/product/chocolate” aria-describedby=”product-title”>
Add to Cart
</a>

A screen reader would announce this as “Add to Cart. Chocolate.”

The aria-labelledby attribute allows us to declare multiple element ids that compose the label.

<div id="myBillingId">Billing</div><div>
<div id="myNameId">Name</div>
<input type="text" aria-labelledby="myBillingId myNameId"/>
</div>

A screen reader would announce the text input as “Billing Name. Edit text.”

Hidden elements

Sometimes there are certain things that should not be recited by screen readers because that same information is accessible elsewhere. To hide an element from screen readers, use the aria-hidden attribute, which will still visually render the element.

<div aria-hidden="true">
The information in here will be visible,
but not accessible via screen readers.
<p>Children will also be hidden</p>
</div>

If you were to use CSS’s display: none; property, the element will be hidden visually and from screen readers.

Images and alternate text

All images that provide context to the page should have thoughtful descriptions in alternate text using the alt property. Close your eyes and imagine you have never seen the image before. How would someone describe the image to you? What are the important bits of the image that help provide context to the page?

Avoid using words like “image,” “illustration,” and “picture” since the user will know they are on an image element.

Inline images

Any image that uses the <img> should have an alt property. If the image doesn’t provide context to the page and is there only for decoration, the alt can be an empty string.

<img src="scenic-background.jpg" alt="" />

But if the image provides additional context to the page, it needs a thoughtful description.

Background images

Most background images are used for decoration only and do not need alternate text. However, sometimes background images include important information and/or provide context. Consider using a hidden div that contains a description of the image.

<div style="background-image: url(…)">
<span class="visually-hidden">
description of background image
</span>
</div>

Alternatively, refactor the code to use <img> tags.

Inline SVGs

You can hide the svg via aria-hidden=”true” and add a visually-hidden element that describes it.

<span class="visually-hidden">ice cream with a cherry on top</span>
<svg aria-hidden="true"></svg>

There are other approaches to make inline SVGs more accessible, but they aren’t widely supported. Some of the approaches are:

  1. Adding role=”img” and aria-label=”…” to the <svg> node.
<svg role="img" aria-label="ice cream with a cherry on top">
...
</svg>

2. Adding a <title> or <desc> node inside of the <svg> that describes the image and setting an id attribute for the node. Then add aria-labelledby to the <svg> that points to the id of the title/desc node.

<svg role="img" aria-labelledby="svg-desc">
<desc id="svg-desc">ice cream with a cherry on top/desc>
</svg>

A screen reader would announce this as “Ice cream with a cherry on top. Image.”

Input labels

Each input should have a <label> associated with it:

<label for="firstname">First name:</label>
<input type="text" name="firstname" id="firstname" />

The screen reader would announce “First name. Edit text.”

Sometimes the <label> should be hidden because of the design, however, it is still necessary to provide a label for an input. In those, situations it can be visually hidden using the previously discussed visually-hidden class:

<label for="lastname" class="visually-hidden">Last name:</label>
<input type="text" name="lastname" id="lastname" />

A screen reader would announce this as “Last name. Edit text.”

Input validation

Inputs often use different styling to denote invalid input. This styling may not be obvious to assistive technologies. Instructing screen readers that a specific input is invalid can be done via the aria-invalid attribute.

<input aria-invalid="true" />

A screen reader would announce this as “Invalid data. Edit text.”

Disabled inputs

The disabled attribute on inputs should be used whenever possible. There are scenarios where the inputs are styled as visually disabled, but don’t have the disabled attribute declared. In those instances, use the aria-disabled attribute so assistive technologies indicate that the input is disabled.. For example, a button needs to be shown as disabled but should still be clickable.

<button aria-disabled="true" class="button-disabled">
Save
</button>

A screen reader would announce this as “Save. Dimmed. Button.”

Alerts

A web page might want to inform the user that something changed on the page. This can be done with specific role values. There are many types of alerts and live regions. For example:

<p role="alert">There was an error saving your changes</p>

A screen reader would announce this as “There was an error saving your changes.”

Live regions

Javascript offers the ability to dynamically change parts of the page without reloading the page. Screen readers will not pick up those changes to the page unless that area is marked as a live region. Instructing screen readers to watch these regions can be done with the aria-live attribute.

HTML

<div id="results-count" aria-live="polite"></div>

JS

function updateResultsCount(count, searchTerm) {
const message = `Found ${count} results for search term: ${searchTerm}`;
document.getElementById("results-count").innerText = message;
}
updateResultsCount(10, "apple")

A screen reader would announce this as “Found 10 results for search term: apple.”

Be aware that if you trigger multiple announcements that contain the same message, some screen readers announce only the first one and ignore subsequent ones. To avoid this announced-once issue, make sure that each alert message is different.

Also note that the container that has aria-live must exist on the DOM before the content within is modified. If the aria-live container is added along with new content, it may not be picked up by some screen readers.

A related attribute is aria-atomic, which indicates to the screen reader whether it should announce:

  • The entire contents when anything changes, or…
  • The individual change by itself

Collapsible and expandable content

A common design pattern is to have a button that reveals some section, like an accordion. The button that performs the action of revealing a section should be annotated with:

  • aria-haspopup to indicate that the button reveals additional content when interacted
  • aria-expanded to indicate the expanded status of the content
<!-- collapsed state -->
<button aria-haspopup="true" aria-expanded="false">
Learn more about food
</button>
<div class="collapsed"></div>

For this collapsed state, a screen reader would announce “Learn more about food. Collapsed. Pop-up button.”

<!-- expanded state -->
<button aria-haspopup="true" aria-expanded="true">
Learn more about food
</button>
<div class="expanded">Lorem ipsum…</div>

In the expanded state, a screen reader would announce “Learn more about food. Expanded. Pop-up button.”

Focus

There will be scenarios where the focus needs to be moved to a specific area on the page. Some of those reasons include:

  • Moving the focus to the invalid input
  • Moving the focus to a newly added element
  • Displaying new search results that were loaded in an infinite scroll list

Moving the focus to an input is easy with javascript via input.focus(), where input represents the element (input, button, textarea, etc.).

It gets a bit more challenging when the focus needs to be moved to a non-interactive component, like a <div>. The simplest fix is to add the tabindex attribute to that destination element.

<div tabindex="-1">my content here</div>

This approach presents a few problems:

  • A non-interactive element will get the focused outline (which can be disabled via CSS outline: 0;).
  • The element may be read out loud, which may not be a desired behavior.
A div with the text of “my content here” that has a focused outline

A way to move the focus, but not force the screen reader to read it out loud, is to create an empty element that can receive focus. Then focus on that element and blur out of it. That will effectively reset the focus to the area below the empty component.

HTML

<div tabindex="-1" id="resetFocus"></div>
<nav>my content here</nav>

JS

const resetFocus = document.getElementById("resetFocus");resetFocus.focus();
resetFocus.blur();

By following this approach, the focus will be moved after the <div> so it will be primed for the next Tab keypress. Be aware that moving focus is effective only for keyboard navigation. The screen reader’s virtual cursor will not be impacted by this approach.

Layout and tab order

Specifying the tabindex of each element on a page by assigning a number that dictates the tab order. However, that approach is hard to maintain, especially on a dynamic page. A more forgiving way is to let the browser dictate the tab order. The tab order often follows the HTML structure:

The focus indicator moves down the list
The focus flow is dictated by the position of the element in the DOM
<a href="https://example.com/1">first selected</a><a href="https://example.com/2">second selected</a><a href="https://example.com/3">third selected</a>

Designs are sometimes complex, and there can be elements on the page that are declared far apart from each other but appear visually together (usually via CSS absolute positioning). Consider the following example. This is a product listing that uses CSS to move “See reviews” to right below the product title.

HTML

<div class="product">
<a href="#">Chocolate Pretzel</a>
<div class="description">
Enjoy the combination of salty,
crunchy pretzels and peanuts with sweet,
rich, chocolaty flavor in these snack bars.
<a href="#">Read more</a>
</div>
<a href="#" class="reviews">See reviews</a>
</div>

CSS

.product {
position: relative;
}
.reviews {
position: absolute;
top: 15px;
}
.description {
margin-top: 20px;
}
The focus indicator navigates links in an unintuitive way
The order of the elements in the HTML doesn’t match the visual order.

This causes the tab order to feel unnatural because the tab order will go from the product title to “Read more” in the description and finally land on “See reviews.” Visually, the expectation would be to go from the product title to “See reviews” and finally “Read more.”

Consider grouping related elements together to maintain a sensible tab order:

<div class=”product”>
<a href="#">Chocolate Pretzel</a>
<a href="#">See reviews</a> <div>
Enjoy the combination of salty,
crunchy pretzels and peanuts with sweet,
rich, chocolaty flavor in these snack bars.
<a href="#">Read more</a>
</div>
</div>

Tools to make accessibility testing easier

Even with accessibility in mind, it’s easy to miss something. These tools have helped the WW team catch issues we would’ve otherwise missed:

  • eslint-plugin-jsx-a11y: A static AST checker for accessibility rules on JSX elements. It informs about missing properties and/or their values.
  • Chrome Lighthouse: This audits for accessibility, performance, progressive web apps, SEO, and more. It is already built into Chrome so it’s easy to get started.
  • axe DevTools: This audits for accessibility issues.

Wrapping it up

The examples in this document demonstrated solutions to make web interfaces more accessible. However, using these examples will not guarantee that the accessible experience is a pleasant one. Please take the time to test the accessible experience of your users; this ensures it is working as designed and it is a pleasant experience.

— Alex Oxrud, engineering manager, web, at WW

Interested in joining the WW team? Check out the Careers page to view technology job listings as well as open positions on other teams.

--

--