Hamburger Menu Using Details-Summary
A hamburger menu built using Details and Summary...
Menu
The CSS...
<style>
*, *:after, *:before {
box-sizing: border-box;
margin: 0;
}
* + * {
margin-top: 1rem;
}
/* The mobile nav styles */
/* Enforce list semantics */
nav [role="list"] {
list-style: none;
padding-left: 1rem;
margin: 0;
}
summary {
border: 1px solid #ccc;
border-radius: 4px;
display: inline-block;
height: 3rem !important;
line-height: 1;
list-style: none;
padding: .5rem !important;
position: relative;
text-align: center;
transition: .4s border-radius ease-out;
width: 3rem;
will-change: border-radius;
}
summary span {
position: relative;
top: .75rem;
transition-delay: 0.2s;
transition-duration: 0s;
}
summary span:before,
summary span:after {
position: absolute;
content: '';
}
summary span,
summary span:before,
summary span:after {
background-color: #000;
display: block;
height: .375rem;
width: 100%;
will-change: background-color, margin, transform;
}
summary span:before {
/* margin-top: -.75rem; */
transform: translateY(-.75rem) rotate(0);
}
summary span:after {
/* margin-top: .75rem; */
transform: translateY(.75rem) rotate(0);
}
summary x {
bottom: -2.125rem;
font-size: 1.2rem;
font-weight: 700;
line-height: 2em;
width: 4rem;
left: 50%;
letter-spacing: .05em;
position: absolute;
text-transform: uppercase;
transform: translate3d(-50%,0%,0);
}
details[open] > summary {
border-radius: 100%;
}
details[open] > summary > span {
background-color: transparent;
transition-delay: 0.2s;
}
summary span:before {
transition-delay: 0.2s, 0s;
transition-duration: 0.2s;
transition-property: margin, transform;
/* transition-property: transform; */
}
details[open] > summary span:before {
margin-top: 0;
transform: rotate(45deg);
/* transform: translateY(0) rotate(45deg); */
transition-delay: 0s, 0.2s;
}
summary span:after {
transition-delay: 0.2s, 0s;
transition-duration: 0.2s;
transition-property: margin, transform;
/* transition-property: transform; */
}
details[open] > summary span:after {
margin-top: 0;
transform: rotate(-45deg);
/* transform: translateY(0) rotate(-45deg); */
transition-delay: 0s, 0.2s;
}
nav details {
display: inline-block;
}
nav summary {
width: auto !important;
border: none;
height: auto !important;
}
nav summary:after {
position: absolute;
content: '▶';
padding-left: 5px;
}
nav details[open] > summary:after {
position: absolute;
content: '▼';
padding-left: 5px;
}
details[open] > summary x {
display: none;
}
@media (min-width: 40em) { /* 640px */
/* Note: dropdowns may use position absolute,
but was considered beyond scope in this simple demo. */
nav > details > [role="list"] {
/* display: flex; */
gap: 1rem;
margin-top: 0 !important;
padding: 0;
}
nav > details > ul > li {
margin-top: 0 !important;
}
}
</style>
The HTML...
<details>
<summary>
<span></span>
<x>Menu</x>
</summary>
<nav>
<ul role=list>
<li><a href="#">About us</a></li>
<li>
<details>
<summary>Products</summary>
<ul role=list>
<li><a href="#">Product 1</a></li>
<li><a href="#">Product 2</a></li>
<li>
<details>
<summary>Sub</summary>
<ul role=list>
<li><a href="#">Sub 1</a></li>
<li><a href="#">Sub 2</a></li>
</ul>
</details>
</li>
</ul>
</details>
</li>
<li><a href="#">Insights</a></li>
<li><a href="#">Contact</a></li>
</ul>
</nav>
</details>
<script>
console.clear();
// The mobile disclosure button only:
// Progressive enhancement, works without JS too.
const responsiveMenuButton = (document => {
'use strict';
const minViewportWidth = '40em'; // @ 640px
const details = document.querySelector('nav > details');
if (!details) return;
const summary = details.querySelector('summary');
if (!summary) return;
const mediaQuery = window.matchMedia(`(min-width: ${minViewportWidth})`);
let isSmallScreen = !mediaQuery.matches;
const showMenu = _ => {
// Edge case: Orientation may change viewport width without moving focus
const isSummaryFocused = !!details.querySelector('summary:is(:focus)');
details.open = true;
summary.hidden = true;
if (isSummaryFocused) {
const firstLink = details.querySelector('a[href]');
firstLink && firstLink.focus();
}
};
const hideMenu = _ => {
// Edge case: Orientation may change viewport width without moving focus
const isMenuLinkFocused = !!details.querySelector('ul:is(:focus-within)');
details.removeAttribute('open');
summary.removeAttribute('hidden');
isMenuLinkFocused && summary.focus();
};
const controlResponsiveMenu = event => {
if (isSmallScreen && event.matches) {
showMenu();
isSmallScreen = true;
}
isSmallScreen && !event.matches && hideMenu();
!isSmallScreen && event.matches && showMenu();
if (!isSmallScreen && !event.matches) {
hideMenu();
isSmallScreen = false;
}
};
mediaQuery.addListener(controlResponsiveMenu);
controlResponsiveMenu(mediaQuery);
})(document);
</script>
The Javascript...
<script>
console.clear();
// The mobile disclosure button only:
// Progressive enhancement, works without JS too.
const responsiveMenuButton = (document => {
'use strict';
const minViewportWidth = '40em'; // @ 640px
const details = document.querySelector('nav > details');
if (!details) return;
const summary = details.querySelector('summary');
if (!summary) return;
const mediaQuery = window.matchMedia(`(min-width: ${minViewportWidth})`);
let isSmallScreen = !mediaQuery.matches;
const showMenu = _ => {
// Edge case: Orientation may change viewport width without moving focus
const isSummaryFocused = !!details.querySelector('summary:is(:focus)');
details.open = true;
summary.hidden = true;
if (isSummaryFocused) {
const firstLink = details.querySelector('a[href]');
firstLink && firstLink.focus();
}
};
const hideMenu = _ => {
// Edge case: Orientation may change viewport width without moving focus
const isMenuLinkFocused = !!details.querySelector('ul:is(:focus-within)');
details.removeAttribute('open');
summary.removeAttribute('hidden');
isMenuLinkFocused && summary.focus();
};
const controlResponsiveMenu = event => {
if (isSmallScreen && event.matches) {
showMenu();
isSmallScreen = true;
}
isSmallScreen && !event.matches && hideMenu();
!isSmallScreen && event.matches && showMenu();
if (!isSmallScreen && !event.matches) {
hideMenu();
isSmallScreen = false;
}
};
mediaQuery.addListener(controlResponsiveMenu);
controlResponsiveMenu(mediaQuery);
})(document);
</script>