Image for post
Image for post

How to Write Accessible JavaScript

Ben Robertson
Oct 9, 2017 · 12 min read
Image for post
Image for post

1. Use the <button> element for anything that users click on.

2. Plan for Common Keyboard interactions.

document.addEventListener('keyup', (event) => {
console.log(event.keyCode);
});
document.addEventListener('keyup', (event) => {
switch (event.keyCode) {
// escape
case 27:
// exit
break;
// enter || spacebar
case 13 || 32:
// submit or something
break;
// left arrow
case 37:
// move back / previous
break;
// right arrow
case 39:
// move forward
break;
// up arrow
case 38:
// move up
break;
// down arrow
case 40:
// move down
break;
}
}

3. Manage ARIA states

<button class="list-expander" aria-expanded="false" aria-controls="expandable-list-1">Expand List</button>
<ul id="expandable-list-1">
<li><a href="http://example.com">Sample Link</a></li>
<li><a href="http://example.com">Sample Link 2</a></li>
<li><a href="http://example.com">Sample Link 3</a></li>
</ul>
const listExpander = document.querySelector('.list-expander');
const list = document.querySelector('#expandable-list-1');
listExpander.addEventListener('click', (e) => {
if(list.getAttribute('aria-expanded') === "true") {
list.setAttribute('aria-expanded', 'false');
} else {
list.setAttribute('aria-expanded', 'true');
}
});

4. Managing Focus

Get the first focusable element.

/** 
* Get all focusable elements inside of the specifed context.
*
* @param {String} [context='document'] The DOM context you want to search in.
* @return {Array} Array of focusable elements
*/
function getFocusable(context = 'document') {
let focusable = Array.from(context.querySelectorAll('button, [href], select, textarea, input:not([type="hidden"]), [tabindex]:not([tabindex="-1"])'));
return focusable;
}
/**
* Get all focusable elements inside of the specified context.
*
* @param {String} [context='document'] The DOM context you want to search in.
* @return {Array} Array of focusable elements
*/

function getFocusable(context = 'document') {
let focusable = Array.from(context.querySelectorAll('button, [href], select, textarea, input:not([type="hidden"]), [tabindex]:not([tabindex="-1"])'));
return focusable;
}
getFirstFocusable(modal).focus();

Ensure that users can easily close the modal via the keyboard when it is open

When the modal closes, return focus to the element that was active when the modal opened.

let previousActiveElement = document.activeElement;
previousActiveElement.focus();

Trap the TAB and SHIFT + TAB inside the modal

/**
* Traps the tab key inside of the context, so the user can't accidentally get
* stuck behind it.
*
* Note that this does not work for VoiceOver users who are navigating with
* the VoiceOver commands, only for default tab actions. We would need to
* implement something like the inert attribute for that (see https://github.com/WICG/inert)
* @param {object} e the Event object
*/

export function trapTabKey(e, context) {
if (e.key !== 'Tab') return;

let focusableItems = getFocusable(context);
let focusedItem = document.activeElement;

let focusedItemIndex = focusableItems.indexOf(focusedItem);

if (e.shiftKey) {
if (focusedItemIndex == 0) {
focusableItems[focusableItems.length - 1].focus();
e.preventDefault();
}
} else {
if (focusedItemIndex == focusableItems.length - 1) {
focusableItems[0].focus();
e.preventDefault();
}
}
}
Image for post
Image for post

Conclusion

Image for post
Image for post

DailyJS

JavaScript news and opinion.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade
A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store