Accessible components: Modals
Carlos Viteri
Posted on March 3, 2020
Modal popups are frequently used on the web. Especially to confirm actions, display ads, handle forms, etc.
However, are you building accessible modals? Can you open and close them by using the keyboard only? Are they understandable for assistive technologies?
If not, no worries, let's build one from scratch.
HTML
<button class="button">Delete item?</button>
<div aria-hidden="true" class="modal">
<div class="modal__backdrop" tabindex="-1">
<div aria-labelledby="modal-title" aria-modal="true" class="modal__container" role="dialog">
<header class="modal__header">
<h2 id="modal-title">Confirm?</h2>
<button aria-label="Close modal">×</button>
</header>
<div class="modal__content">
<p>This is the description of an accessible modal</p>
</div>
<footer class="modal__footer">
<button>Yes</button>
<button aria-label="Close modal">No</button>
</footer>
</div>
</div>
</div>
In the snippet above, basically we have two nodes: the trigger button and the modal.
aria
and role
attributes explanation:
aria-hidden="true"
this attribute removes its content from the accessibility tree. We must toggle this attribute to "false"
once we open the modal.
aria-labelledby="id"
will tell assistive technologies that content of the id, in this case the heading level 2, is what describes the dialog.
aria-modal="true"
informs assistive technologies that content outside that element is inert.
role="dialog"
helps assistive technology identify the dialog's content as being grouped and separated from the rest of the page content. However, this attribute alone is not enough, it must be properly labeled and must manage the keyboard focus correctly.
aria-label
is used to define a string that labels the current element. In the buttons above we are using this attribute to explain better their purposes, it is necessary only when the text inside the button is not explanatory enough.
The first button has no aria
attribute because the text inside is sufficient to explain the purpose of the button.
CSS
.modal[aria-hidden="true"] {
display: none;
}
.modal__backdrop {
align-items: center;
background-color: rgba(0, 0, 0, 0.6);
bottom: 0;
display: flex;
justify-content: center;
left: 0;
position: fixed;
right: 0;
top: 0;
}
.modal__container {
background-color: white;
max-height: 100vh;
max-width: 400px;
overflow-y: auto;
padding: 20px;
}
.modal__header {
align-items: center;
display: flex;
justify-content: space-between;
}
First of all, we are hiding the modal if it has the attribute aria-hidden="true"
. Then we are creating a backdrop to set up enough contrast between the modal and the rest of the page. And finally we are preventing the vertical content to get hide of the viewport by adding a Y axis scroll if the content is higher than 100vh
.
JavaScript
const focusableElements = [
'[contenteditable]',
'[tabindex]:not([tabindex^="-"])',
'a[href]',
'area[href]',
'button:not([disabled]):not([aria-hidden])',
'embed',
'iframe',
'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',
'object',
'select:not([disabled]):not([aria-hidden])',
'textarea:not([disabled]):not([aria-hidden])'
];
const modal = document.querySelector('.modal');
const openButton = document.querySelector('.button');
const closeButtons = document.querySelectorAll('.modal__close-button');
document.addEventListener('keydown', handleKeydown);
openButton.addEventListener('click', openModal);
closeButtons.forEach(button =>
button.addEventListener('click', closeModal)
);
function handleKeydown(e) {
const esc = 27;
if (e.keyCode === esc) {
closeModal();
}
}
function openModal() {
const focusableModalElements = modal.querySelectorAll(focusableElements);
modal.setAttribute('aria-hidden', 'false');
if (focusableModalElements.length) {
focusableModalElements[0].focus();
}
}
function closeModal() {
modal.setAttribute('aria-hidden', 'true');
}
In the JavaScript part we will toggle the aria-hidden
attribute depending on what we want to do. When opening the modal we must focus the first focusable element within the modal. And obviously we should support the esc
key to close the modal.
Working example:
Posted on March 3, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.