Skip to content

Instantly share code, notes, and snippets.

@baerkins
Last active August 24, 2018 19:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save baerkins/4dee92b6d7b0afc7fa346fb2aedcfa88 to your computer and use it in GitHub Desktop.
Save baerkins/4dee92b6d7b0afc7fa346fb2aedcfa88 to your computer and use it in GitHub Desktop.
Accessible Modal with JS
body.is-locked {
height: 100%;
overflow: hidden;
}
.modal {
display: none;
}
.modal--is-open {
display: block;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #fff;
z-index: 2;
}
<!-- Example Header -->
<header role="banner">
<div class="container">
<a href="/" class="logo">Some Website Logo</a>
<nav class="primary-nav" role="navigation">
<span id="primary-nav-title" class="sr-only">
<ul aria-labelledby="primary-nav-title">
<li><a href="/about" role="menuitem">About</a></li>
</ul>
</nav>
<!--
Modal Toggle Button
Match `data-modalid` to the ID of the modal element to toggle
-->
<button class="more-nav--toggle _modal-toggle" data-modalid="more-menu">Open Nav</button>
</div>
</header>
<!-- /Example Header -->
<!--
Modal
Use div (section can behave strangely for some reason with screen readers)
Give the modal it's own H1 - modals should be treated as though they are their own page
Close button should be the first focusable element
-->
<div id="more-modal" class="more-menu modal" role="dialog" aria-modal="false" tabindex="-1">
<div class="container">
<h1 class="sr-only">Modal Title</h1>
<button class="more-nav--close _modal-toggle" data-modalid="more-modal">Close Nav</button>
<!-- Add modal content stuff -->
</div>
</div>
<!-- /Modal -->
//
// Configurable Classes
//
const modalOpenClass = 'modal--is-open';
const modalReturnFocus = 'modal--will-focus';
//
// Placeholders for modal focusable elements
//
let focusableEls = null,
firstFocusableEl = null,
lastFocusableEl = null;
window.siteHasOpenModal = false;
/**
* Open modal
* Add modal--open class to modal, set aria-modal to true, set focus on modal
* add modal--return-focus class to trigger
*
* @param {modalID} modalID ID of target modal
* @param {object} trigger The javascript object that triggered the event
*
*/
function openModal(modalID, trigger) {
if ( window.siteHasOpenModal ) {
return false;
}
window.siteHasOpenModal = true;
const modal = document.getElementById(modalID);
modal.classList.add(modalOpenClass);
modal.setAttribute('aria-modal', true);
document.body.classList.add('is-locked');
trigger.classList.add(modalReturnFocus);
// Determine focusable elements
focusableEls = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
firstFocusableEl = focusableEls[0];
lastFocusableEl = focusableEls[focusableEls.length-1];
firstFocusableEl.focus();
};
/**
* Close modal
* Remove modal--open class to modal, set aria-modal to false
* remove modal--return-focus class to first element, focus it
*
* @param {string} modalID ID of target modal
*
*/
function closeModal(modalID) {
const modal = document.getElementById(modalID);
modal.classList.remove(modalOpenClass);
modal.setAttribute('aria-modal', false);
const returnFocus = document.getElementsByClassName(modalReturnFocus)[0];
returnFocus.focus();
returnFocus.classList.remove(modalReturnFocus);
window.siteHasOpenModal = false;
document.body.classList.remove('is-locked');
};
/**
* Handle Keydown events when a modal is open
*
* @param {event} e keydown event
*
*/
function keydownModal(e) {
// Return if a modal is not open
if ( !window.siteHasOpenModal ) {
return false;
}
// Grab the first modal
const modal = document.getElementsByClassName('modal ' + modalOpenClass)[0];
// Allow Escape to close window modal
if (e.keyCode === 27 ) {
closeModal( modal.getAttribute('id') );
// Tab Trap on open modals
} else if (e.key === 'Tab' || e.keyCode === 9) {
// shift + tab
if ( e.shiftKey ) {
if (document.activeElement === firstFocusableEl) {
lastFocusableEl.focus();
e.preventDefault();
}
// tab
} else {
if (document.activeElement === lastFocusableEl) {
firstFocusableEl.focus();
e.preventDefault();
}
}
}
}
/**
* modalInit
* Generalized modal mechanics providing accessible features
*
* Example Markup:
* <button class="_modal-toggle" data-modalid="some-modal">Toggle</button>
* <div id="some-modal" class="modal" role="dialog" aria-modal="false"><!-- Stuff --></div>
*
* <!-- Open State -->
* <button class="_modal-toggle modal--return-focus" data-modalid="some-modal">Toggle</button>
* <div id="some-modal" class="modal modal-open" role="dialog" aria-modal="true"><!-- Stuff --></div>
*
*/
export const modalInit = () => {
const modalButtons = document.getElementsByClassName('_modal-toggle');
[].forEach.call(modalButtons, (btn) => {
btn.addEventListener('click', () => {
const modalID = btn.dataset.modalid;
const modal = document.getElementById(modalID);
if (modal.classList.contains(modalOpenClass)) {
closeModal(modalID);
} else {
openModal(modalID, btn);
}
});
});
// Handle keydown events for open modals
window.addEventListener('keydown', keydownModal, true);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment