Andrei Brumaru
December 5, 2022
{time} min read

A Comprehensive Web Accessibility Guide for Web Developers


Web Accessibility - a term often overlooked in the early days of the web which is gaining more and more prevalence every day. Since the technological ecosystem evolved in a way in which it ended up in everyone's hands, regardless of where or who they are in the world, and the web being a core part of almost every piece of technology - the idea that the web should be well designed for everyone gained more traction and is a well regarded standard in many places and institutions nowadays. That's what accessibility means -> making something easily accessible and removing as many hurdles as possible imposed by environmental or physical conditions. In this article, we'll touch on many general accessibility ideas which can be transferred to other tech areas as well, but we'll be drilling down on Web Accessibility specifically from the perspective of a Software Developer or Quality Assurance Engineer.

While the Americans with Disabilities Act does not specifically address Website accessibility specifically, historically disputes or lawsuits were highly in favour of companies which abided by the Web Content Accessibility Guidelines (WCAG 2.1) - so much so that it basically became the norm for designing highly accessible UIs.

While the guidelines are very well described, the document can be difficult to navigate through - thus a more practical resource to reference while building a compliant experience is the WebAIM's WCAG 2 Checklist with references to the original document.

A list of useful links:

Common acronyms used throughout article:

  • a11y - Accessibility
  • SR - Screen Reader
  • ARIA - Accessible Rich Internet Applications

A11y Conceptual Guidelines

Core principles of web a11y - POUR

A good thing to keep in mind while developing accessible apps is the P.O.U.R. principles (Perceivable, Operable, Understandable, Robust). Whenever working on a change or feature, it’s good to form the habit of asking yourself if your work complies with these principles.

  1. Perceivable - Web content must be available to the senses (sight, hearing and/or touch)
  2. Operable - Interface forms, controls, and navigation are operable
  3. Understandable - Information and the operation of user interface must be understandable
  4. Robust - It can be interpreted by a wide variety of user agents, including assistive technologies

Tackling the core principles systematically

The POUR principles are generally achievable in most cases - and a good way to make sure this happens is to split the a11y compliance work into 3 main categories.

1. Semantics

Building with good semantics and maintaining them consistent throughout the application is most crucial aspect of a11y and the one that should be addressed first - as it solves a great deal of issues most of the time in the most common situations.

Similar to the DOM, the browser creates an Accessibility Tree. Just like the DOM gets translated to a visual representation of your semantics on the screen, the Accessibility Tree gets translated to other types of representations understandable by assistive technology, such as screen readers.

Semantics can be a deep-dive topic, but at the end of the day it generally boils down to using the right HTML elements and attributes. The most important types of elements to keep in mind are:

  • Headings - Headings structure is the most important aspect for visually impaired users. They should be structured in a logical order by what they represent, not by their size or style which is a very common mistake.
  • Links - Should be coherent in all cases. What might be straightforward for a sighted user may not be for someone using a SR. A common example would be links that contain an images instead of text should have well described alt texts. Another one would be that links which rely on the context they're visually placed in might benefit from a more descriptive text (such as "Learn more about turtles" instead of just "Learn more").
  • Landmarks - Specific HTML elements which convey more information for building the a11y tree. They are basically just elements used for grouping together relevant pieces of content, making it easier for assistive software to better understand what those pieces of content represent. A list and descriptions for each of them can be found here, but for reference some of the most common ones are:
  • {{{<header>}}}, {{{<main>}}}, {{{<footer>}}} - Determines core website areas and what each area’s conceptual role is
  • {{{<nav>}}} - Used for grouping links that achieve website or page content navigation
  • {{{<section>}}} - completely generic, used for delimiting different sections of an app
  • ARIA attributes - They allow us to basically modify / do surgical interventions on the a11y tree, without affecting any other kind of behaviour such as styling or functionality. A common one you may already have seen would be aria-label which overrides any other labelling mechanism, suitable for elements that have a visual indication but no text associated to them. Another one would be aria-hidden which when set to true, removes the element from the a11y tree.
    A comprehensive list of all aria-* attributes can be found here.
    Sometimes the aria-* attributes need to go hand in hand with role attributes to achieve an accessible experience, which can be found here.

2. Focus

Focus determines where the keyboard / assistive technology events go on the page. All interactable elements should be focusable and navigatable in a logical and coherent fashion through keyboard inputs and assistive technology inputs.

A few important principles to keep in mind:

  • Logical Tab Order - The Tab Order is the automatic order generated by the browser for focusable elements. It is mirrored to the DOM order, which means the order of the elements in the page DOM takes priority over the visual order of the elements on the page affected by styling. The Tab Order should be logical and consistent with the visual order of the elements on the page, and can be manually changed through using the tabindex attribute.
  • Prevent off-screen elements from stealing focus - Elements that are not visible on the screen should not be focusable at that particular point in time. A common example would be a slide-in navigation menu which can be either visible or hidden, but is not removed from the DOM when hidden. In this case we should make sure that while the menu is hidden it is not focusable.
  • Avoid accidental keyboard traps - The user should always be able to tab through the pages' controls and not be trapped in a certain area of the page. Though there are some examples in which you may want to intentionally trap the user in a specific set of controls - for example when displaying a modal dialog you would want the user to only be able to tab through the controls inside the modal until it gets closed and prevent them from interacting with the other controls in the background.

3. Design

The website’s design should be easily perceivable and interactable by people with certain visual or motor impairments. This can be achieved through paying attention to a few aspects such as:

  • Text size - text should be readable on all devices. Guidelines dictate it should not be of values of under 12px, and should be zoomable up to 200%.
  • Zoom functionality and scale - zoom functionality should not be disabled, especially on mobile devices. The content should scale similarly and stay consistent on multiple devices. This can be achieved through properly setting the viewport meta tag and proper responsive design.
  • Touch targets are large enough - for users with motor disabilities it may be difficult to accurately tap a small control on a touch enabled device. The guidelines state that a touch target should be no smaller than 48 dp (device pixels).
  • Color purpose and contrast - some visual impairments might affect the perception of colours for certain users, or make it difficult for them to distinguish between colours that don’t have a high enough in contrast. This means that cases in which color is method of conveying content or distinguishing visual elements should be avoided (for example an on/off status represented solely through the color of a control). Also, regarding color contrast for text elements, it is generally recommended to try to aim for a 4.5:1 contrast ratio or higher.

Cheatsheet / Common questions

How do I check if a website is accessible?

A11y compliance on a website is not a yes or no. A website/application can be less or more accessible -

There are tools that help us check for a11y issues. While they are recommended and very helpful, they will not manage to detect some issues in more complicated modern UI design patterns. For example, it is easy for a tool to figure out wether the contrast between the color of a text and it’s background high enough, but is virtually impossible to figure out wether your new fancy custom date-picker component is easily navigable by a SR user and makes sense to them. The first example is deterministic - as it can follow a strict rule which can always be applied the same way, but the second one is much less so - any two custom date-pickers from two different applications may have completely different behaviour so we would have to judge if the experience provided is accessible enough or not.

That being said, we should use tools for checking a11y, but we should also manually check and make decisions regarding what an accessible experience should be.

1. Using Tools

The most popular a11y testing tool is aXe: axe DevTools - Web Accessibility Testing

A good start would be here: Getting started with the axe DevTools extension

Another good one is Chrome dev tools' built-in “Lighthouse” tab. A good overview of how it works can be found here (content is a bit outdated, you don’t need to manually enable it anymore): The new way to test accessibility with Chrome DevTools - A11ycasts #23

2. Manual audit

Performing these steps below will generally reveal most a11y issues in the most common cases, but do remember that manually auditing a website involves making decisions and judgement calls along the way as there are no strict rules about how any given custom non-standard component should behave. We will have to decide if a certain section of our website is accessible enough or not based on our experience during the audit.

  1. Ensure you can navigate with just the keyboard and that there are discernible focus styles.
    - The focus order should make sense
    - Make sure there are no focusable elements off-screen
  2. Ensure you can properly navigate with a SR
    - The focus order should make sense
    - Images should have alt texts
    - Custom controls should behave like native ones and work with SR inputs (ex: a <div> acting as a custom button should be interpreted and interacted with just like a native <button>)
    - If there's dynamic content added to the screen, there should be a way for the SR to announce it and/or focus it.
  3. Check the HTML page structure.
    - Headings should be properly enumerated- no heading level should be skipped.
    - Appropriate landmark roles and landmark elements are used.
    - Check the page with the "Rotor" (for Voiceover - Mac) or "Elements list" (for NVDA - Windows) which provides an outline of the page.
  4. Check for color contrasts (using one of the above mentioned tools).

A very handy resource to reference while performing audits is WebAIM: WebAIM's WCAG 2 Checklist. While it is not a definitive guide, it does a very good job of condensing the most important aspects of WCAG2.

How to manage focus and/or tab order?

By default the natural tab order is identical to the order of the elements in the DOM. This means that if we build our DOM in a comprehensive manner according to our content, we will generally have pretty good results in terms of a11y compliance. However, this is not always possible and situations in which you will have to manage focus are common in modern SPAs. A few examples may include:

  • When displaying a modal window, you may want to direct focus inside the window restrict it from leaking outside of it.
  • When showing/hiding a collapsible navigation menu, you may want to enable/disable the focusability of the menu’s elements.

The tabindex attribute enables us to manage page elements' focus in a custom manner. It also enables handling focus related functionalities for elements which are not focusable by default.

  • {{{tabindex=”0”}}} - the element is focusable and is present in the natural tab order.
  • {{{tabindex=”-1”}}} - the element is focusable programatically, but is not present in the natural tab order. Tabbing through will not focus the element.
  • {{{tabindex=”>1”}}} - the element is focusable and takes priority in the natural tab order. The higher the number, the higher the priority.

A tabindex value of higher than 0 is considered an anti-pattern. While it modifies the natural tab order for regular users, it will not affect the order in which SRs interpret the content, thus creating inconsistencies experiences between different users navigating in different ways.

Check out this example which shows the difference between how a set of native buttons differs from a set of custom ones (low a11y), as well as the fixes for the custom ones to make them more accessible.

Code example: A11y - Custom Buttons

What aria-* / role attributes should I use to make my custom components accessible?

In cases in which we may not be able to use native controls or proper semantics when writing our applications, we should ensure that the end result will still provide an accessible experience. Thus, we should pay attention to “decorating” our HTML with the proper aria-* and role attributes which convey more underlying information to SRs and other assistive technology about what the your code is supposed to do.

A very helpful resource to reference is the WAI-ARIA Authoring Practices 1.1 which tells us exactly what to take into account when implementing common patterns and widgets and how they should behave. It describes how the Keyboard Interaction should behave and which WAI-ARIA Roles, States, and Properties you should use.

The best way to understand how to use this document is by example. In the below example you can see a custom implementation of a Radio Group without using the native <input type=”radio”> - to achieve that we should reference the Radio Group definition - WAI-ARIA Authoring Practices 1.1. Based on the specifications, you can see a side-by-side comparison of a native implementation, a custom one with low a11y, and a custom one with high a11y:

Code example: A11y - Custom RadioGroup

A highlight of the most commonly used aria-* labels can be found below:

  • {{{aria-label="string"}}} - overrides any other labelling mechanism, suitable for elements that have a visual indication but no text associated to them.
  • {{{aria-labelledby="id"}}} - refer to a different element (through id) which will be used as the label for any given element.
  • {{{aria-hidden="true"}}} - hides the element from the a11y tree.
  • {{{aria-owns="id"}}} - describes a parent-child relationship between 2 elements for the a11y tree.

<div id="menu" aria-owns="submenu">...</div>
<div id="submenu">...</div>
  • {{{aria-describedby="id"}}} - used for presenting more description for elements on the page that might need it, besides just the label.

<label for="pw">Password</label>
<input type="password" id="pw" aria-describedby="pw-help">
<div id="pw-help">Password must have at least 12 characters</div>
  • {{{aria-checked="bool"}}} - wether a control is checked or not (for where it may apply).
  • {{{aria-expanded="bool"}}} - wether a control is expanded or not (for where it may apply).
  • {{{aria-live="off|polite|assertive"}}} - whenever the element’s content changes or enters the screen, the SR will announce it.
    off = no announcement
    polite = finish current task and make announcement
    assertive = stop everything and make announcement.
    A good use case for this attribute would be toast style notifications. For example an important error such as “Could not connect” could be using aria-live="assertive".
  • {{{aria-busy="true"}}} - tells SR to ignore content changes this element (for example while a loading animation is present and content may be updating in the background)
  • {{{aria-atomic="true"}}} - wether a parent's children should all be read together (by SR) when a change is made to them. A good use case would be a time picker where it would make sense for the SR to always read both the hour and minute values whenever a change is made to either value.

<div class="timepicker" aria-atomic="true">
	<input type="number" id="hours" value="12">
	<input type="number" id="minutes" value="30">

A full list of all {{{aria-*}}} attributes can be found here: ARIA Attributes

A full list of the role definitions can be found here: Role Definitions

How do I use a SR (Screen Reader)?

Here you can find guides for some of the most commonly used SRs:


Since a11y is not something you need to tackle every single day as an web engineer, it's easy for your grasp on the core concepts to loosen over time and forget things. I initially wrote this as a cheatsheet for a project's dev team to use when developing frontend applications which needed an extra degree of care on the a11y side of things. But it grew more and more over time and got to a point in which it was ready to be turned into an article and shared with the world. At Quickleaf, we still reference it to this day and do our best to keep it updated. I hope many of you will find it useful!


our work