code
Accessibility in HTML and JavaScript
This chapter describes best practices for designing HTML/JavaScript applications that work well for all users, including those who rely on assistive technologies. We covered most common use cases in our components with built-in accessibility features.
For a detailed introduction into basic questions and general techniques for designing accessible applications, see the accessibility section of the mdn web docs.
Accessibility attributes
Building accessible web experiences often involves setting ARIA attributes to provide semantic meaning where it might otherwise be missing. Use JavaScript to dynamically control the values of accessibility-related attributes.
When you work with ARIA attributes in JavaScript, use the setAttribute() method or direct property assignment:
// Use setAttribute for ARIA attributes
const button = document.querySelector('button');
button.setAttribute('aria-label', 'Save document');
// Or use direct property assignment
button.ariaLabel = 'Save document';
Static ARIA attributes can be set directly in HTML:
<!-- Static ARIA attributes require no extra syntax -->
<button aria-label="Save document"><i class="icon element-save"></i></button>
For iX components, use the dedicated ariaLabel attribute for elements contained inside the component. For example, setting the aria-label for the icon-buttons contained in ix-date-picker.
Keyboard only and no keyboard trap
All functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes, except where the underlying function requires input that depends on the path of the user's movement and not just the endpoints.
For components that use a simple, linear structure, stick to the default tab-based navigation. Make sure every clickable surface is both reachable and clickable by keyboard. You can also set the tabindex attribute to manipulate the default tab-navigation.
For components that use a more complex, two-dimensional structure, consider implementing keyboard interaction using the arrow keys:
// Arrow key navigation for complex components
function handleArrowNavigation(event, container) {
const items = Array.from(container.querySelectorAll('[role="gridcell"], [role="option"]'));
const currentIndex = items.indexOf(event.target);
switch(event.key) {
case 'ArrowRight':
focusItem(items[currentIndex + 1] || items[0]);
break;
case 'ArrowLeft':
focusItem(items[currentIndex - 1] || items[items.length - 1]);
break;
case 'ArrowDown':
// Implement based on your layout
break;
case 'ArrowUp':
// Implement based on your layout
break;
}
}
function focusItem(item) {
if (item) {
item.focus();
}
}
Always attempt to keep the focus on changes to prevent the user from manually having to navigate back to the previous location.
Focus management
Implement focus trapping for modal dialogs and other overlay components:
function trapFocus(container) {
const focusableElements = container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
const firstFocusable = focusableElements[0];
const lastFocusable = focusableElements[focusableElements.length - 1];
container.addEventListener('keydown', function(event) {
if (event.key === 'Tab') {
if (event.shiftKey) {
if (document.activeElement === firstFocusable) {
lastFocusable.focus();
event.preventDefault();
}
} else {
if (document.activeElement === lastFocusable) {
firstFocusable.focus();
event.preventDefault();
}
}
}
});
}
How to test
Disconnect your mouse and try to operate your service using only the keyboard.
Text alternatives and labels
Any non-text content (images, buttons, links, inputs) must be labelled to be WCAG 2.1 compliant. The general rule to follow is:
- Use
aria-labelledby - Otherwise use
aria-label - Otherwise use
altattribute - Otherwise use
titleattribute - If none of the above yield a usable text string, there is no accessible name
When you are using iX components, the relevant attribute will be set when you set the component's labelling attribute:
ARIA-labelledby
Use when there is already a text which describes the element. An example are forms where there is a field description followed by the input.
<p id="input-name-label">This is the input label</p>
<input aria-labelledby="input-name-label" />
ARIA-label
The aria-label describes elements that have no text, like images, buttons or links. The interaction result or impact
of clicking / activating a button or link shall be explained by the aria-label.
<!-- Static ARIA attributes require no extra syntax -->
<button aria-label="Save document"><i class="icon element-save"></i></button>
You can also set these dynamically with JavaScript:
const button = document.querySelector('#save-button');
button.setAttribute('aria-label', 'Save document');
Alt attribute
Use alt attributes some elements offer to add an additional description to an element. Depending on the screen reader it might not get picked up.
<img src="chart.png" alt="Sales increased by 25% in Q3" />
Title attribute
Similar to the alt attribute, the title allows adding additional information about an element. Again, it might not be caught up by screen readers.
<button title="Save document"><i class="icon element-save"></i></button>
Visually hidden text
When e.g. aria-label isn’t allowed or doesn't make sense to use, use hidden text to make specific description text that is read by a screen reader but isn’t visible in the UI.
<i class="icon element-physical-input" aria-hidden="true"></i><span class="visually-hidden">physical input</span>
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
How to test
If you are using chrome dev tools you have some options that make your life easier here. You can either use the audit functionality to generate a report or use the accesibility tree.
Alternatively, you can use browser extensions or playwright in conjunction with @axe-core/playwright within the CI/CD Pipeline. Additionally, using a screen reader to test it is beneficial.
Navigation and landmarks
Navigation provides a mean for the user to get around on the website. Most of the HTML sectioning elements provide default ARIA landmarks.
The aria-label attribute enables the logical navigation definition and separation of elements, which have the same type.
For example, if multiple <nav> elements are present on a page, use the aria-label attribute.
<nav aria-label="Main"></nav>
<nav aria-label="Secondary"></nav>
The navigation role can also be used instead of the <nav> element:
<div role="navigation" aria-label="Customer service">
<ul>
<li><a href="#">Help</a></li>
<li><a href="#">Order tracking</a></li>
<li><a href="#">Shipping & Delivery</a></li>
<li><a href="#">Returns</a></li>
<li><a href="#">Contact us</a></li>
<li><a href="#">Find a store</a></li>
</ul>
</div>
Lists
Lists must be correctly structured to be recognized by screen readers. This means that parent (<ul> or <ol>) must be directly followed by <li> and no other content element is allowed to be in-between.
Correct:
<ul>
<li>
<p>text</p>
</li>
<li></li>
</ul>
Incorrect:
<ul>
<div>This breaks the list structure</div>
<li>List item</li>
</ul>
Or use one of the lists:
Roles
Roles help identifying the purpose of an element on the website. Most of the native html elements already contain the role. However, if there is a need for more complex structures, the roles must be set explicitly.
An example is a custom built menu. Setting the roles is important for the screen reader to distinguish the elements and announce them properly.
<ul role="menu">
<li role="presentation">Heading</li>
<li role="separator">
<div class="divider"></div>
</li>
<li></li>
<li role="presentation">
<a role="menuitem" href="#">Link</a>
</li>
</ul>
You can also set roles dynamically with JavaScript:
const menuItem = document.createElement('a');
menuItem.setAttribute('role', 'menuitem');
menuItem.href = '#';
menuItem.textContent = 'Menu Item';
See WAI-ARIA Roles for more information.
Language of a page and parts
Set the language of the human readable text for a web page or each paragraph or phrase via the lang attribute:
<html lang="en">
<p>This is written in English</p>
<p lang="de">Das ist in Deutsch geschrieben</p>
</html>
You can also set language dynamically with JavaScript:
document.documentElement.lang = 'en';
// Or for specific elements
document.querySelector('#german-content').lang = 'de';
Using semantic HTML elements
Native HTML elements capture several standard interaction patterns that are important to accessibility. When building web applications, you should re-use these native elements directly when possible, rather than re-implementing well-supported behaviors.
For example, instead of creating a custom element for a button, use the native <button> element:
<!-- Good: Uses semantic button -->
<button onclick="saveDocument()">
<i class="icon save"></i>
Save
</button>
<!-- Avoid: Non-semantic div that requires extra ARIA -->
<div role="button" tabindex="0" onclick="saveDocument()" onkeydown="handleKeydown(event)">
<i class="icon save"></i>
Save
</div>
Live regions for dynamic content
Use ARIA live regions to announce dynamic content changes to screen readers:
<div id="status" aria-live="polite" aria-atomic="true"></div>
function announceStatus(message) {
const statusDiv = document.getElementById('status');
statusDiv.textContent = message;
}
// Usage
announceStatus('Document saved successfully');
For urgent announcements, use aria-live="assertive":
<div id="error-status" aria-live="assertive" aria-atomic="true"></div>
Mobile accessibility
Mobile accessibility is crucial as mobile devices are increasingly the primary way users access digital content. Modern mobile platforms like iOS and Android have built-in accessibility tools, making it essential to ensure your web applications work seamlessly with these technologies.
Mobile-specific considerations
When developing accessible mobile applications, focus on these key areas:
Control mechanisms
Ensure interface controls work across different interaction methods:
// Support both touch and mouse events
function handleInteraction(element) {
// Mouse events
element.addEventListener('mousedown', startAction);
element.addEventListener('mouseup', endAction);
// Touch events for mobile
element.addEventListener('touchstart', (e) => {
// Prevent mouse events from firing
e.preventDefault();
startAction(e);
});
element.addEventListener('touchend', (e) => {
e.preventDefault();
endAction(e);
});
// Keyboard support
element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
startAction(e);
}
});
}
Avoid mouse-specific events that don’t work on touch devices:
// ❌ Avoid: Mouse-only events
div.onmousedown = startDrag;
document.onmouseup = stopDrag;
// ✅ Better: Multi-modal support
function addDragSupport(element) {
// Mouse support
element.addEventListener('mousedown', initiateDrag);
// Touch support
element.addEventListener('touchstart', initiateDrag);
// Keyboard support
element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
initiateDrag(e);
}
});
}
User input optimization
Minimize typing requirements on mobile devices:
<!-- Use semantic input types for better mobile experience -->
<input type="tel" placeholder="Phone number" />
<input type="email" placeholder="Email address" />
<input type="number" placeholder="Age" />
<input type="date" placeholder="Birth date" />
<!-- Provide options instead of free text when possible -->
<select aria-label="Job title">
<option>Software Engineer</option>
<option>Product Manager</option>
<option>Designer</option>
<option value="other">Other</option>
</select>
<!-- Show additional field only when "Other" is selected -->
<input type="text" id="other-job" placeholder="Please specify" style="display: none;" />
// Progressive enhancement for job selection
const jobSelect = document.querySelector('select[aria-label="Job title"]');
const otherInput = document.getElementById('other-job');
jobSelect.addEventListener('change', function() {
if (this.value === 'other') {
otherInput.style.display = 'block';
otherInput.focus();
otherInput.setAttribute('required', 'true');
} else {
otherInput.style.display = 'none';
otherInput.removeAttribute('required');
}
});
Responsive design for accessibility
Ensure your layouts work across different screen sizes and orientations:
/* Ensure text remains readable when zoomed */
@media screen and (max-width: 768px) {
body {
font-size: 16px; /* Minimum readable size */
line-height: 1.5;
}
/* Ensure touch targets are large enough */
button, input, select, textarea {
min-height: 44px;
min-width: 44px;
}
/* Provide adequate spacing between interactive elements */
.interactive-element {
margin: 8px 0;
}
}
/* Support high-resolution displays */
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
.icon {
/* Use SVG or high-resolution images */
background-image: url('icon@2x.png');
background-size: contain;
}
}
Viewport configuration
Always enable zooming for accessibility:
<!-- ✅ Correct: Allow user scaling -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
<!-- ❌ Never disable zoom -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
Mobile Navigation Patterns
Implement accessible hamburger menus and mobile navigation:
<nav role="navigation" aria-label="Main navigation">
<button
class="menu-toggle"
aria-controls="mobile-menu"
aria-expanded="false"
aria-label="Toggle navigation menu">
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
</button>
<ul id="mobile-menu" class="mobile-menu" aria-hidden="true">
<li><a href="#home">Home</a></li>
<li><a href="#about">About</a></li>
<li><a href="#contact">Contact</a></li>
</ul>
</nav>
function initializeMobileMenu() {
const menuToggle = document.querySelector('.menu-toggle');
const mobileMenu = document.getElementById('mobile-menu');
menuToggle.addEventListener('click', toggleMenu);
menuToggle.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleMenu();
}
});
function toggleMenu() {
const isExpanded = menuToggle.getAttribute('aria-expanded') === 'true';
menuToggle.setAttribute('aria-expanded', !isExpanded);
mobileMenu.setAttribute('aria-hidden', isExpanded);
mobileMenu.classList.toggle('open');
// Focus management
if (!isExpanded) {
// Focus first menu item when opening
const firstMenuItem = mobileMenu.querySelector('a');
if (firstMenuItem) {
firstMenuItem.focus();
}
}
}
// Close menu when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('nav')) {
const isExpanded = menuToggle.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
toggleMenu();
}
}
});
// Handle escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
const isExpanded = menuToggle.getAttribute('aria-expanded') === 'true';
if (isExpanded) {
toggleMenu();
menuToggle.focus();
}
}
});
}
.menu-toggle {
background: none;
border: none;
padding: 12px;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: space-around;
width: 44px;
height: 44px;
}
.hamburger-line {
width: 20px;
height: 2px;
background-color: currentColor;
transition: all 0.3s ease;
}
.mobile-menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ccc;
transform: translateY(-100%);
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}
.mobile-menu.open {
transform: translateY(0);
opacity: 1;
visibility: visible;
}
.mobile-menu a {
display: block;
padding: 16px;
text-decoration: none;
border-bottom: 1px solid #eee;
min-height: 44px;
display: flex;
align-items: center;
}
.mobile-menu a:focus {
outline: 2px solid #0066cc;
outline-offset: -2px;
}
Screen reader testing
Android TalkBack
To test with TalkBack:
- Enable TalkBack in Settings > Accessibility
- Use these gestures:
- Single tap: Select and announce item
- Double tap: Activate selected item
- Swipe left/right: Navigate between items
- Two-finger swipe up: Access global menu
- Swipe up then right: Access local menu
iOS VoiceOver
To test with VoiceOver:
- Enable in Settings > Accessibility > VoiceOver
- Use these gestures:
- Single tap: Select item
- Double tap: Activate item
- Swipe left/right: Navigate between items
- Three-finger swipe: Scroll
- Rotor gesture: Access navigation options
Testing guidelines
Manual testing checklist
☑ All interactive elements have minimum 44px touch targets
☑ Content is readable when zoomed to 200%
☑ Navigation works with touch, keyboard, and screen readers
☑ Form inputs use appropriate mobile keyboards
☑ No horizontal scrolling on mobile devices
☑ Focus indicators are visible and clear
☑ Screen reader announces all important content changes
Automated testing
// Example Playwright test with axe-core
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('mobile accessibility compliance', async ({ page }) => {
await page.goto('/your-page');
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('touch target sizes', async ({ page }) => {
await page.goto('/your-page');
await page.setViewportSize({ width: 375, height: 667 });
const interactiveElements = page.locator('button, a, input, select, textarea');
const count = await interactiveElements.count();
for (let i = 0; i < count; i++) {
const element = interactiveElements.nth(i);
const box = await element.boundingBox();
expect(box.width).toBeGreaterThanOrEqual(44);
expect(box.height).toBeGreaterThanOrEqual(44);
}
});
Mobile-specific ARIA considerations
When developing for mobile pay special attention to:
<!-- Announce loading states -->
<div aria-live="polite" aria-atomic="true" id="loading-status"></div>
<!-- Provide clear button labels -->
<button aria-label="Add item to shopping cart">
<i class="icon-plus" aria-hidden="true"></i>
</button>
<!-- Group related form fields -->
<fieldset>
<legend>Shipping Address</legend>
<input type="text" aria-label="Street address" />
<input type="text" aria-label="City" />
<input type="text" aria-label="Postal code" />
</fieldset>
// Announce dynamic content changes
function updateLoadingStatus(message) {
const statusElement = document.getElementById('loading-status');
statusElement.textContent = message;
}
// Usage
updateLoadingStatus('Loading search results...');
setTimeout(() => {
updateLoadingStatus('Search completed. Found 25 results.');
}, 2000);
Key takeaways
- Test on actual devices with screen readers enabled
- Ensure adequate touch target sizes (minimum 44px)
- Support multiple input methods (touch, keyboard, voice)
- Allow zoom and scaling for users with visual impairments
- Optimize form inputs for mobile keyboards
- Implement proper focus management in mobile navigation
- Test with both portrait and landscape orientations
- Provide clear feedback for all user actions