<a href="#main" class="skip-link sr-only sr-only-focusable">Skip to main content</a>
<header>
<nav class="header-nav" aria-label="Main">
<button class="header-nav__open" type="button" aria-expanded="false" aria-controls="menu">Menu</button>
<div class="header-nav__container" id="menu">
<button class="header-nav__close" type="button">Close</button>
<ul>
<li><a href="#">Lorem</a></li>
<li><a href="#">Architecto</a></li>
<li><a href="#">Quas</a></li>
<li><a href="#">Nobis</a></li>
<li><a href="#">Eligendi</a></li>
<li><a href="#">Quaerat</a></li>
</ul>
</div>
<div class="header-nav__curtain"></div>
</nav>
</header>
<main id="main" tabindex="-1">
<h1>AFTER Example Page</h1>
<p>
Header nav on Desktop, Slideout nav on mobile! Part of my
<a href="https://stevenwoodson.com/blog/solving-animation-layout-flickering-caused-by-css-transitions/">
"Solving Animation Layout Flickering Caused by CSS Transitions"</a> article.
</p>
<p>
This example fixes the
flash of menu styles when resizing from desktop to mobile.
<a href="https://codepen.io/stevenwoodson/pen/abjGjrr">Check this
pen out if you'd like to see the before example</a>.
</p>
<p>
This example is intentionally left lean on styles so you can take and
adapt it to your needs more easily. Would love to see what you come up
with, <a href="https://stevenwoodson.com/">send me a message</a>!
</p>
</main>
<footer>© 2023 Awesome Site</footer>
$bp_mobile_max: 479px;
$bp_tablet_min: 480px;
$global-spacing: 20px;
$menu-width: 240px;
/* ==========================================================================
Relevant Menu Styles
========================================================================== */
.header-nav {
#menu {
background: white;
ul {
list-style: none;
margin: 0;
padding: 0;
}
li {
padding: $global-spacing;
}
@media (max-width: $bp_mobile_max) {
bottom: 0;
box-shadow: 0 0 12px 1px #00000075;
height: calc(100% - 40px);
left: 0;
margin: 0;
min-width: $menu-width;
opacity: 0;
overflow: auto;
padding: 20px;
position: absolute;
transform: translateX(-100%);
visibility: hidden;
z-index: 100;
}
@media (min-width: $bp_tablet_min) {
li {
display: inline-block;
}
}
}
&__close {
padding: 0 $global-spacing;
@media (min-width: $bp_tablet_min) {
display: none;
}
}
&__open {
padding: 0 $global-spacing;
@media (min-width: $bp_tablet_min) {
display: none;
}
}
&__curtain {
background: rgba(0, 0, 0, 0.6);
height: 100vh;
left: 0;
opacity: 0;
position: absolute;
top: 0;
transition: all 0.5s ease;
visibility: hidden;
width: 100%;
z-index: 3;
}
#menu.nav-opening {
transition: all 0.5s ease;
}
#menu.nav-opened {
opacity: 1;
transform: translateX(0);
visibility: visible;
& + .header-nav__curtain {
opacity: 1;
visibility: visible;
}
}
}
/* ==========================================================================
Other styles for this example page
========================================================================== */
body {
margin: 0 auto;
max-width: 800px;
}
header, main, footer {
padding: $global-spacing;
}
main {
outline: 0;
padding-bottom: 100px;
&:focus {
outline: 0;
}
}
:focus,
a:focus,
button:focus,
input:not([type='checkbox']):not([type='radio']):focus,
select:focus,
textarea:focus {
outline: 4px dashed #dc3542;
outline-offset: 4px;
}
a.skip-link {
transition: none;
&:active,
&:focus {
left: 50%;
padding: 1em;
position: absolute;
top: 10px;
transform: translateX(-50%);
}
}
.sr-only {
border: 0;
clip: rect(0, 0, 0, 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.sr-only-focusable {
&:active,
&:focus {
clip: auto;
height: auto;
margin: 0;
overflow: visible;
position: static;
width: auto;
}
}
View Compiled
(function () {
'use strict';
function SlideOutMenu(id) {
this.el = document.querySelector(id);
this.curtain = document.getElementsByClassName('header-nav__curtain')[0];
this.closeButton = document.getElementsByClassName(
'header-nav__close'
)[0];
this.openButton = document.getElementsByClassName(
'header-nav__open'
)[0];
this.main = document.querySelector('main');
// Find all focusable children
const focusableElementsString =
'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable]';
let focusableElements = this.el.querySelectorAll(focusableElementsString);
focusableElements = Array.prototype.slice.call(focusableElements);
this.firstTabStop = focusableElements[0];
this.lastTabStop = focusableElements[focusableElements.length - 1];
// Add event listeners
this.closeButton.addEventListener('click', this.menuClose.bind(this));
this.curtain.addEventListener('click', this.menuClose.bind(this));
this.openButton.addEventListener('click', this.menuOpen.bind(this));
this.el.addEventListener('transitionend', this.setFocus.bind(this));
this.el.addEventListener('keydown', this.keydownEvent.bind(this));
}
SlideOutMenu.prototype.menuOpen = function (e) {
e.preventDefault();
this.el.classList.add('nav-opening');
this.el.classList.add('nav-opened');
this.openButton.setAttribute("aria-hidden","true");
this.openButton.setAttribute("aria-expanded","true");
this.main.setAttribute("aria-hidden","true");
};
SlideOutMenu.prototype.menuClose = function (e) {
e.preventDefault();
this.el.classList.remove('nav-opened');
this.main.removeAttribute("aria-hidden");
this.openButton.removeAttribute("aria-hidden");
};
SlideOutMenu.prototype.setFocus = function (e) {
if (e.target == this.el && e.propertyName == 'opacity') {
const compStyles = window.getComputedStyle(this.el);
if (compStyles.opacity == 1) {
this.firstTabStop.focus();
} else {
this.openButton.focus();
this.el.classList.remove('nav-opening');
}
}
};
SlideOutMenu.prototype.keydownEvent = function (e) {
// Escape key should close the menu
if (e.key === 'Escape') {
this.menuClose(e);
}
// Tab key check for first or last tab stop
if (e.key === 'Tab') {
// Tab + Shift (reverse tabbing)
if (e.shiftKey) {
// If the current item is the first tab stop, or it shares a name with it (for example a set of radio buttons)
if (
document.activeElement === this.firstTabStop ||
(document.activeElement.name !== '' &&
document.activeElement.name === this.firstTabStop.name)
) {
e.preventDefault();
this.lastTabStop.focus();
}
} else if (document.activeElement === this.lastTabStop) {
e.preventDefault();
this.firstTabStop.focus();
}
}
};
const menu = new SlideOutMenu('#menu');
})();
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.