Web Accessibility

Includes (non-exhaustive):
- zoom-friendly layouts
- keyboard navigation
- contrast
- screen reader usage
- impairments, which may be permanent or temporary
- vision impairments
- hearing impairments
- motor acuity or dexterity impairments
- cognitive impairments (e.g. dyslexia)

WCAG - Web Content Accessibility Guidelines

Four core principles (POUR):

  • Perceivable - across various senses
  • Operable - can users use the UI & navigate the content? (e.g. hover interactions are inaccessible by those that don't use a mouse)
  • Understandable - can uses understand the UI & content w/o confusion?
  • Robust - for content to be consumed across various user-agents & assistance technologies

(see WCAG checklist)


Focus determines where keyboard events go in the page; i.e. focusing on elements, e.g. by clicking, tab key, etc.

A lot of web accessibility deals with managing focus.

Tab moves focus forward; Shift+Tab moves focus backwards; arrow keys move focus w/in a component.

Tab order determines the order focus moves through on the page.

HTML inputs & interactive elements are implicitly focusable, i.e. are automatically in the tab order (based on position in the DOM; CSS can change the visual order w/o changing the DOM order) and have built-in keyboard event handling.

The tabindex attribute can be used to manually insert something into the tab order.
- tabindex="-1" is not in the "natural" tab order but can be manually focused via JS's focus()
- tabindex="0" places it in the natural tab order
- tabindex values > 0 can mess up the tab order, so avoid this!

As a general rule, tabindex should only be added to interactive elements.

One exception: when content changes due to user interaction, e.g. changing tabs in a single page app. In these cases you'd use tabindex="-1" and call focus() on it after the interaction.

Focus on a page often starts with header & other nav elements, not the main content. This can mean a lot of unnecessary tabbing for users. One common practice is to add a hidden "skip link" as the first interactive element that can be used to skip focus to the main content.


<a href="#main-content" class="skip-link">Skip to main content</a>
<main id="main-content" tabindex="-1"></main>

.skip-link {
  position: absolute;
  top: -40px;
.skip-link:focus {
  top: 0;


When making custom interactive components, refer to the ARIA Design Patterns doc, which lists keyboard shortcuts for HTML widgets, which you should implement.

Roving focus: when focus in an element rotates, e.g. in a set of radio buttons.

Dealing w/ off-screen elements (e.g. off-screen menus):
- document.activeElement tells you the currently-focused element
- Chrome Accessibility Dev Tools extension can also help with this
- Using display: none or visibility: hidden will resolve this

Keyboard trap: when a user cannot tab out of the focused element. Normally, this is undesirable, but it is desirable for some cases, eg. modals where you don't want focus to leave the modal. Currently no easy way to do this (some ideas have been proposed); you have to implement it yourself.

Note: when you close a modal, it should restore focus to the previously-focused element (if appropriate).

Assistive technology

Includes: screen readers, text-to-speech, voice control, Braille display, etc...

Screen Readers read out for an element:
- role
- name, if present
- value, if present
- state, if present (e.g. "selected")

"Implicit semantic meaning" - when using standard HTML elements, a lot of the above semantics come by default.

Provide text alternative for any non-text content, e.g. name, title, label, alt text. Note that alt is different than title, i.e. alt text is shown if the image is broken and should be a description of the image, not just a title.

Screen readers follow DOM order, not visual order, so bear that in mind, e.g. w/ headings.

Link anti-patterns

  • using non-anchor elements styled as links with an onClick handler
  • using anchor elements with no href attribute
  • using an image as the link content. Just make sure to use alt text for the image.

In the first two cases, you should use a button element.

The purpose of a link should be discernible from the link text alone, without assuming surrounding context.


Landmarks for quick navigation:

  • the main element (should only be one)
  • the header element
  • the footer element
  • the nav element
  • the article element (rule of thumb: use if the content makes sense in different contexts)
  • the section element
  • the aside element (e.g sidebars)


Web Accessibility Initiative - Accessible Rich Internet Applications; for accessibility that is currently beyond the scope of native HTML.

E.g. if we had to create a custom checkbox, a screen reader wouldn't know our checkbox is meant to be equivalent to the HTML checkbox. We can tell it this with role="checkbox" and aria-checked="true". These do not modify element behavior or implement keyboard shortcuts, etc.

ARIA also lets us create new widgets that aren't just recreations of existing HTML elements.

ARIA has an alert role which a screen reader may read immediately.

ARIA labels can be used to add help description text to any element:

  • aria-label
  • aria-labelledby, to identify an element A as the label for element B. This can take multiple elements, but does not provide the clicking behavior HTML labels provide, e.g. for checkboxes.
  • aria-describedby, e.g. password requirements hint


<button aria-label="Close">X</button>

There are some limitations on what ARIA roles can be used w/ what HTML elements. E.g. an <input type="text"> can't have role="heading". Generally, ARIA can't conflict w/ implicit semantics.

Relationship attributes

aria-labelledby is an example of a "relationship attribute", which defines a semantic relationship b/w two elements.

aria-owns is another which says that an element that's separate in the DOM should be treated as another element's child, e.g:

<div aria-owns="submenu"></div>
<div id="submenu"></div>

Another is aria-activedescendant.

Elements w/ the display:none or visibility:hidden CSS or the hidden HTML attribute will be hidden from assistive technologies. Hidden elements related w/ e.g. aria-describedby will still be read.


aria-live indicates an element that will have its content updated and should be communicated to the user. It can be off (default, polite (will wait until next free moment), or assertive (will interrupt). aria-live elements should be part of the initial page load.

Related attributes:
- aria-atomic: if true, the region is treated as a whole on change. Otherwise, only the changed part is read.
- aria-relevant: indicates what types of changes are read, e.g. additions, removals, text, all, additions text (default)
- aria-busy: temporarily ignore changes to this element


  • styling for focus and ARIA states
    • you can replace the default focus ring, but there should always be some focus indicator
  • color & contrast
  • minimum touch target size: 48dp (device-independent pixels)
    • this can be accomplished via padding rather than resizing the image itself
  • suggested 32dp margin around touch target
  • recommended text contrast ratio of 4.5:1 (at least)
    • for large text (>18pt or 14pt bold), 3:1
    • for low vision impairments, 7:1 and 4.5:1 for regular/bold suggested instead (called "enhanced" contrast)
  • color should not be used as the sole method for conveying information (e.g. a form field that has an error should also include an error message rather than only being highlighted in red)
  • should also text how your page looks in High Contrast mode