Skip to main content

Accessibility

Accessibility ensures that digital products are usable by everyone, including people with disabilities. It is crucial for creating inclusive solutions that meet diverse needs and comply with standards like WCAG, enhancing the overall user experience.

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:

  1. Use aria-labelledby
  2. Otherwise use aria-label
  3. Otherwise use alt attribute
  4. Otherwise use title attribute
  5. 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 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 &amp; 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:

Event list

Card list

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:

  1. Enable TalkBack in Settings > Accessibility
  2. 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:

  1. Enable in Settings > Accessibility > VoiceOver
  2. 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

  1. Test on actual devices with screen readers enabled
  2. Ensure adequate touch target sizes (minimum 44px)
  3. Support multiple input methods (touch, keyboard, voice)
  4. Allow zoom and scaling for users with visual impairments
  5. Optimize form inputs for mobile keyboards
  6. Implement proper focus management in mobile navigation
  7. Test with both portrait and landscape orientations
  8. Provide clear feedback for all user actions