Accessibility (a11y)
Accessibility Checklist: https://webaim.org/standards/wcag/checklist
1. Semantic Markup
Organize the content meaningfully, and avoid using <div>
and <span>
tags everywhere.
- Structural Blocks / Landmarks (
<article>
,<section>
,<aside>
,<main>
,<header>
,<footer>
,<nav>
— a list featuring links and a nested <ul> tag) . - Headings (
<h1>
to<h6>
). - Lists (
<ul>
,<ol>
,<dl>
,<menu>
— a list contains buttons; Equivalent to a <ul> tag) . - Links (
<a>
). - Forms (
<form>
,<label>
,<input>
,<button>
). Use form elements with proper labeling (<label>
) to create accessible forms instead of <p> for titles/names of fields. Limitations with the<label>
tag:<button>
,<input>
,<keygen>
,<meter>
,<output>
,<progress>
,<select>
,<textarea>
. - Tables (
<table>
,<th>
,<tr>
,<td>
). - Images (
<img>
). Use thealt
attribute with descriptive text for images. Screen readers use thealt
text to convey the content and purpose of images to users who cannot see them. If there is no ‘alt’ attribute, screen readers will announce the ‘src’ attribute.
1.1. Creating an element invisible to users but accessible to screen readers.
This technique is often used to provide additional context or information for users who rely on screen readers.
<div class="visually-hidden">
This content is hidden from sighted users but accessible to screen readers.
</div>
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
white-space: nowrap; /* Added for preserving space for single line content */
}
1.2. Focus Management
1.2.1. Focus
All interactive elements must display their focus state when navigating with tabs.
You can access the active element through scripts.
// When the active element is present
const activeElement = document.activeElement
// You can save it and show a popup element to manipulate it further.
// After closing the popup add focus to the previous one
activeElement.focus()
1.2.2. Tab Navigation
Ensure that all interactive elements are accessible for tab navigation.
- Tab — to the next element.
- Tab + Shift — to the previous element.
Tabbable content (by default): (1) Links (<a href=”…”>), (2) Form Inputs: Such as <input>, <textarea>, and <select>, as long as they are not disabled, (3) Buttons (<button>): Unless they are disabled, (4) Iframes (<iframe>): Can be focused and allow interaction within the frame, (5) Editable Content ([contenteditable=”true”]): Elements with the contenteditable attribute set to true, (6) Widgets: Some ARIA widgets like sliders, checkboxes, and radio buttons, when appropriately coded
(!) Add arrow key navigation for child elements that appear after interacting with the Tab key. Popups, dropdowns, hidden menues etc.
1.2.3. tabindex
tabindex
is an important attribute in HTML used to manage keyboard focus, making web content more accessible, especially for users who rely on keyboard navigation.
tabindex="0"
:
This value places the element in the natural tab order. It’s particularly useful for elements that are not natively focusable (like <div>
, <span>
, and <p>
). Example: <div tabindex="0">Focusable Div</div>
makes the div
focusable without disrupting the flow of document tab order.
tabindex="-1"
:
This allows an element to be focusable programmatically (through JavaScript) but not via keyboard navigation. It’s useful for managing focus in dynamic applications, like when a modal opens and you want to set focus to its content for screen reader users. Example: <div tabindex="-1">Programmatically Focusable Div</div>
.
tabindex="1"
:
Avoid using this. It can make navigation less predictable and more complex.
1.2.4. Tab Trapping
Tab trapping is a technique used in web development to confine the keyboard focus within a specific area of a webpage, typically used in modal dialogs or popups.
Steps: (1) Detect when the focus is about to leave the designated area. (2) Redirect the focus back to the first or last element within the area, depending on the direction of tab navigation.
<div id="modal" tabindex="-1">
<input type="text" placeholder="Input 1">
<input type="text" placeholder="Input 2">
<button>Click Me</button>
<a href="#">Link</a>
</div>
const modal = document.getElementById('modal')
const focusableElements =
'button, [href], input, select, textarea,
[tabindex]:not([tabindex="-1"])'
const firstFocusableElement = modal.querySelectorAll(focusableElements)[0]
const focusableContent = modal.querySelectorAll(focusableElements)
const lastFocusableElement = focusableContent[focusableContent.length - 1]
document.addEventListener('keydown', function(e) {
const isTabPressed = e.key === 'Tab' || e.keyCode === 9
if (!isTabPressed) {
return
}
if (e.shiftKey) { // if shift key pressed for shift + tab combination
if (document.activeElement === firstFocusableElement) {
lastFocusableElement.focus() // add focus for the last focusable element
e.preventDefault()
}
} else { // if tab key is pressed
// if focused has reached to last focusable element
if (document.activeElement === lastFocusableElement) {
firstFocusableElement.focus() // add focus for the first focusable element
e.preventDefault()
}
}
})
firstFocusableElement.focus()
1.2.5. Keyboard Shortcuts
You can customize keyboard shortcuts for your application to enable content manipulation solely using the keyboard. It should be well-documented and should not replace standard key combinations as Cntr + C, Cntr + V etc.
1.2.6. Skip Links
While navigating with the Tab key, you can display an additional button that allows users to skip repetitive content, such as long navigation etc.
2. ARIA Tags
SPEC: https://www.w3.org/TR/html-aria/#aria-table
The first rule of ARIA use: If you can use semantic markup instead, do so! In other cases, use ARIA.
For example, use a <button>
element for buttons instead of a <div>
with role="button"
.
The second one: No ARIA is Better Than Bad ARIA.
You might need it when:
- Roles: use
role
if there is no html5 analogs. Such a tab, tabpanel, checkbox, alert, banner etc.
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques#roles
<div class="alert-message" role="alert">Alert text</div>
- Labels:
area-label
to describe elements.
aria-label
(inline elements) and aria-labelledby
(for block elements) provide accessible names, while aria-describedby
offers a longer extended description.
<p>
Download <a aria-label="Github page for repository" href="...">this repo</a>
</p>
<!-- or -->
<div id="some-id" class="hidden">Github page for repository</div>
<a aria-labelledby="some-id" href="...">this repo</a>
- Dynamic / Live Content.
https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques#live_region_roles
ARIA is particularly useful for dynamic content (like modal dialogs, notifications, and dynamic form validation) where traditional HTML might not convey changes to assistive technologies.
aria-live
options:
polite
— will announce the live region update when its next idles.
assertive
— will interrupt whatever it’s doing to announce.
off
— will not read the update.
...</form>
<div id="status"
aria-live="polite"
aria-atomic="true">{{ Some dynamic content from JS }}</div>
Live Region (aria-live="polite"
): The div
with id="status"
is marked as a live region.
aria-atomic="true"
: This attribute ensures that the entire content of the live region is announced as a single atomic update, rather than just the changed part.
- Properties and States.
Provide additional information about elements, such as whether a dropdown is expanded (aria-expanded="true"
or "false"
) or an element is disabled (aria-disabled="true"
). Should be changed in scripts depending on the state.
- Relationships between elements.
ARIA attributes to enhance the accessibility of a navigation menu structure.
<nav id="main-nav">
<button id="menu-button-1"
aria-haspopup="true"
aria-controls="menu-list-1">Menu List 1</button>
<ul id="menu-list-1" role="menu" aria-labelledby="menu-button-1">
<li role="none"><a role="menuitem" href="...">Link 1</a></li>
<li role="none"><a role="menuitem" href="...">Link 2</a></li>
</ul>
</nav>
aria-haspopup="true"
indicates that this button controls the display of a popup menu.
aria-controls="menu-list-1"
establishes an association between the button and the menu it controls.
This ul
(unordered list) is identified as a menu using role="menu"
.
The li
(list item) elements have role="none"
to remove the default list item role. This is often done in complex menu structures to ensure proper interpretation by screen readers, especially when using role="menu"
and role="menuitem"
.
<a role="menuitem" href="...">
. The anchor (a
) elements within the list items are assigned role="menuitem"
. This role explicitly defines each link as an item in a menu, which can be particularly important for screen readers to convey the nature of the list items in this specific context.
3. Color Contrast
Contrast Checker (for the design stage): https://webaim.org/resources/contrastchecker/
Use the Lighthouse Chrome tool to test the current state. The Color Tool in the DevTools also have build-in ability to measure the contrast.
Color contrast is a critical aspect of accessibility, ensuring that text and interactive elements have sufficient visual distinction from their background to be easily readable.
4. Language Preference
It’s important to add the lang
attribute to the <html>
tag or other elements for localization to set up the language of the reader.
<html lang="en">
...
<blockquote lang="es">
...
</blockquote>
5. Prefers Reduced Motion
This media query is commonly used to check whether the user has requested the system to minimize animations or motion effects due to preferences related to motion sensitivity or motion-related disabilities.
.animated-element {
animation: exampleAnimation 2s ease-in-out infinite;
}
/* Adjust styles when user prefers reduced motion */
@media (prefers-reduced-motion: reduce) {
.animated-element {
animation: none; /* Disable animation for reduced motion preference */
}
}
6. Tools
JSX-Linter: https://github.com/jsx-eslint/eslint-plugin-jsx-a11y
Systems:
- Google Material Design https://m3.material.io/
- Adobe’s React Spectrum https://react-spectrum.adobe.com/
Dev Tool: Chrome Lighthouse