A simple fullscreen overlay menu
This is a quick set up for a lightweight (vanilla JS only) and accessible full screen overlay menu.
The requirements for this are pretty straightforward:
- activate the overlay via a menu open button
- trap focus within the open overlay until it is closed
- the close button should receive focus immediately on opening of the overlay
- the open button should receive focus immediately on closing of the overlay
- the scroll position of the page beneath the overlay should not change between opening and closing the overlay
Setting up the page
The set up is pretty straightforward: the overlay, a button to open the overlay and a button to close it.
Here’s the overlay and the close button it contains:
<div class="c-nav-overlay" id="nav-overlay" role="navigation" aria-expanded="false">
<button type="button" name="close" class="c-nav-toggle c-nav-toggle--closer" id="nav-closer" aria-controls="nav-overlay">Menu</button>
<!—- nav menu goes here -—>
</div>
And the button to open the overlay:
<button type="button" name="open" class="c-nav-toggle c-nav-toggle--opener" id="nav-opener" aria-controls="nav-overlay">Menu</button>
The overlay needs some CSS to handle both the inactive hidden view and the subsequent active open view:
.c-nav-overlay {
visibility: hidden;
position: fixed;
z-index: 800;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0;
background-color: #000000;
color: #ffffff;
overflow-x: hidden;
overflow-y: scroll;
-webkit-overflow-scrolling: touch;
transition: opacity 0.2s, visibility 0.2s;
}
.c-nav-overlay--open {
opacity: 1;
visibility: visible;
}
Controlling the scrolling
We can set the <body>
to overflow: hidden;
to prevent the main page beneath from scrolling when the overlay is open.
Here’s the CSS to do that and to show the overlay:
body.is-overlay-open {
overflow: hidden;
}
.is-overlay-open .c-menu-overlay {
opacity: 1;
visiblity: visible;
}
Trapping the focus
To trap the focus within the overlay when it’s open, I used this great script which did the job perfectly.
With that in place, all that’s left to do is toggle a couple of classes to add the overlay open and close functionality.
/*
Trap focus function for the overlay
https://hiddedevries.nl/en/blog/2017-01-29-using-javascript-to-trap-focus-in-an-element
*/
function trapFocus(element) {
var focusableEls = element.querySelectorAll('a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select'),
firstFocusableEl = focusableEls[0];
lastFocusableEl = focusableEls[focusableEls.length - 1];
KEYCODE_TAB = 9;
element.addEventListener('keydown', function(e) {
var isTabPressed = (e.key === 'Tab' || e.keyCode === KEYCODE_TAB);
if (!isTabPressed) {
return;
}
if ( e.shiftKey ) /* shift + tab */ {
if (document.activeElement === firstFocusableEl) {
lastFocusableEl.focus();
e.preventDefault();
}
} else /* tab */ {
if (document.activeElement === lastFocusableEl) {
firstFocusableEl.focus();
e.preventDefault();
}
}
});
}
/*
onclick functions to launch the overlay, toggle the required classes and grab the current scroll position of the page
*/
document.addEventListener("DOMContentLoaded", function() {
var overlay = document.getElementById('nav-overlay');
var scrollPos = document.querySelector("body").scrollTop;
document.querySelector('.c-nav-toggle--opener').addEventListener("click", function() {
document.querySelector(".c-nav-overlay").classList.toggle("c-nav-overlay--open");
document.querySelector("body").classList.toggle("is-overlay-open");
document.getElementById("nav-overlay").setAttribute("aria-expanded", "true");
document.getElementById('nav-closer').focus();
trapFocus(overlay);
});
document.querySelector('.c-nav-toggle--closer').addEventListener("click", function() {
document.querySelector(".c-nav-overlay").classList.toggle("c-nav-overlay--open");
document.querySelector("body").classList.toggle("is-overlay-open");
document.getElementById("nav-overlay").setAttribute("aria-expanded", "false");
document.getElementById('nav-opener').focus();
document.querySelector("body").scrollTop = scrollPos; // [1]
});
});
Here’s a quick demo Codepen.
Note
There’s always something… it looks like overflow scrolling on the body
isn’t prevented as hoped / expected on iOS (at least on versions <12, iOS won’t prevent users from scrolling past the modal if there is content that exceeds the viewport height). Using position: fixed;
on the open overlay while also getting the window scroll position at overlay open and returning it to the <body>
on overlay close as per the demo will mitigate against this.
And yeah, there is probably a way more elegant way to handle the class toggling events on the buttons…