<!-- for use with <use> -->
<svg xmlns="http://www.w3.org/2000/svg" hidden>
<symbol id="arrow" viewbox="0 0 16 16" >
<polyline points="4 6, 8 10, 12 6" stroke="#000" stroke-width="2" fill="transparent" stroke-linecap="round" />
</symbol>
</svg>
<!-- In the real world, all hrefs would have go to real, unique URLs, not a "#" -->
<nav id="site-navigation" class="site-navigation" aria-label="Clickable Menu Demonstration">
<ul class="main-menu clicky-menu no-js">
<li>
<a href="#">Home</a>
</li>
<li>
<a href="#">
Services
<svg aria-hidden="true" width="16" height="16">
<use xlink:href="#arrow" />
</svg>
</a>
<ul>
<li><a href="#">Design</a></li>
<li><a href="#">Development</a></li>
<li><a href="#">Accessibility</a></li>
<li><a href="#">Content Strategy</a></li>
<li><a href="#">Training</a></li>
</ul>
</li>
<li>
<a href="#">
Portfolio
<svg aria-hidden="true" width="16" height="16">
<use xlink:href="#arrow" />
</svg>
</a>
<ul>
<li><a href="#">Nonprofits</a></li>
<li><a href="#">Higher Education</a></li>
<li><a href="#">Associations</a></li>
<li><a href="#">Consultants</a></li>
</ul>
</li>
<li>
<a href="#">
About
<svg aria-hidden="true" width="16" height="16">
<use xlink:href="#arrow" />
</svg>
</a>
<ul>
<li><a href="#">Mission</a></li>
<li><a href="#">History</a></li>
<li><a href="#">Contact</a></li>
</ul>
</li>
</ul>
</nav>
<p class="github"><a href="https://github.com/mrwweb/clicky-menus">Now on Github</a></p>
/**
* Initial state, hidden off screen
*/
.clicky-menu ul {
position: absolute;
top: 100%;
left: 0;
visibility: hidden; /*[1]*/
}
/**
* No JS fallback
*
* Triggers menus on hover rather than click. Supports keyboard navigation in modern browsers.
*/
.clicky-menu.no-js li:hover > ul {
visibility: visible;
}
.clicky-menu.no-js li:focus-within > ul { /*[2]*/
visibility: visible;
}
/**
* Open/Close Menu Behavior with JS
*/
.clicky-menu ul[aria-hidden="false"] {
visibility: visible;
}
/* Prevent offscreen-submenus */
.clicky-menu .sub-menu--right {
left: auto !important;
right: 0 !important;
}
/**
* Footnotes
*
* [1] Using `visibility` instead of `display` allows for easier transitions and animation of submenus
* [2] Must be a separate ruleset so that hover works in non-modern browsers
*/
/* DEMO CSS */
body {
margin: 0;
min-height: 100vh;
background: linear-gradient(17deg, teal, purple, orange) center/cover no-repeat;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, Oxygen-Sans, Ubuntu, Cantarell,
"Helvetica Neue", sans-serif;
}
/* Hidden SVG used for down arrows */
svg[hidden] {
display: none;
position: absolute;
}
.site-navigation {
width: 86%;
max-width: 782px;
margin: 100px auto 300px;
box-shadow: 2px 2px 4px rgba(0,0,0,.2);
background-color: #eee;
border-radius: 4px;
}
.clicky-menu {
justify-content: stretch;
margin: 0;
padding: 0;
list-style: none;
}
@media (min-width: 540px) {
.clicky-menu {
display: flex;
}
}
/* General Link & Button Styles */
.clicky-menu a,
.clicky-menu button {
margin: .25em;
padding: 1em;
background: transparent;
color: #000;
font-weight: bold;
text-decoration: none;
font-family: inherit;
border-radius: 3px;
}
.clicky-menu a:hover,
.clicky-menu button:hover {
background: #fff;
}
.clicky-menu a:focus,
.clicky-menu button:focus {
outline: .125em dotted purple;
outline-offset: -.125em;
}
/* Top Level Items */
.clicky-menu > li {
position: relative;
flex: 1 1 auto;
display: flex;
justify-content: stretch;
flex-wrap: wrap;
}
.clicky-menu > li > a,
.clicky-menu > li > button {
flex: 1 0 auto;
display: flex;
flex-wrap: wrap;
align-items: center;
border: 0;
font-size: inherit;
line-height: 1.5;
cursor: pointer;
}
@media (min-width: 540px) {
.clicky-menu > li > a,
.clicky-menu > li > button {
justify-content: center;
}
}
/* Icon */
.clicky-menu svg {
width: 1em;
height: 1em;
margin-left: .5em;
}
.clicky-menu [aria-expanded="true"] svg {
transform: scaleY(-1);
}
/* Submenu Styles */
.clicky-menu ul {
min-width: 100%;
width: 12em;
margin-top: .25em;
padding: 0;
list-style: none;
background-color: #eee;
border-radius: 3px;
}
@media (min-width: 540px) {
.clicky-menu {
box-shadow: 2px 4px 4px rgba(0,0,0,.2);
}
}
/* Responsive Submenu Behavior */
.clicky-menu ul[aria-hidden="false"] {
position: static;
width: 100%;
flex: 0 0 auto;
}
@media (min-width: 540px) {
.clicky-menu ul[aria-hidden="false"] {
position: absolute;
width: auto;
}
}
/* Submenu Links */
.clicky-menu ul a {
display: block;
padding-top: .375em;
padding-bottom: .375em;
}
@media (min-width: 540px) {
.clicky-menu ul a {
padding: .375em 1em;
white-space: nowrap;
}
}
.github {
text-align: center;
a {
color: #fff;
opacity: .6;
&:hover,
&:focus {
opacity: 1;
}
}
}
View Compiled
/**
* Object for creating click-triggered navigation submenus
*
* Latest version, Issues, etc: https://github.com/mrwweb/clicky-menus
*
* Thanks for the inspiration:
* - https://www.lottejackson.com/learning/a-reusable-javascript-toggle-pattern
* - https://codepen.io/lottejackson/pen/yObQRM
*/
(function() {
'use strict';
const ClickyMenus = function( menu ) {
// DOM element(s)
let container = menu.parentElement,
currentMenuItem,
i,
len;
this.init = function() {
menuSetup();
document.addEventListener( 'click', closeOpenMenu );
}
/*===================================================
= Menu Open / Close Functions =
===================================================*/
function toggleOnMenuClick( e ) {
const button = e.currentTarget;
// close open menu if there is one
if ( currentMenuItem && button !== currentMenuItem ) {
toggleSubmenu( currentMenuItem );
}
toggleSubmenu( button );
};
function toggleSubmenu( button ) {
const submenu = document.getElementById( button.getAttribute( 'aria-controls' ) );
if ( 'true' === button.getAttribute( 'aria-expanded' ) ) {
button.setAttribute( 'aria-expanded', false );
submenu.setAttribute( 'aria-hidden', true );
currentMenuItem = false;
} else {
button.setAttribute( 'aria-expanded', true );
submenu.setAttribute( 'aria-hidden', false );
preventOffScreenSubmenu( submenu );
currentMenuItem = button;
}
};
function preventOffScreenSubmenu( submenu ) {
const screenWidth = window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth,
parent = submenu.offsetParent,
menuLeftEdge = parent.getBoundingClientRect().left,
menuRightEdge = menuLeftEdge + submenu.offsetWidth;
if ( menuRightEdge + 32 > screenWidth ) { // adding 32 so it's not too close
submenu.classList.add( 'sub-menu--right' );
}
}
function closeOnEscKey(e) {
if( 27 === e.keyCode ) {
// we're in a submenu item
if( null !== e.target.closest('ul[aria-hidden="false"]') ) {
currentMenuItem.focus();
toggleSubmenu( currentMenuItem );
// we're on a parent item
} else if ( 'true' === e.target.getAttribute('aria-expanded') ) {
toggleSubmenu( currentMenuItem );
}
}
}
function closeOpenMenu( e ) {
if ( currentMenuItem && ! e.target.closest( '#' + container.id ) ) {
toggleSubmenu( currentMenuItem );
}
};
/*===========================================================
= Modify Menu Markup & Bind Listeners =
=============================================================*/
function menuSetup() {
menu.classList.remove('no-js');
menu.querySelectorAll('ul').forEach( ( submenu ) => {
const menuItem = submenu.parentElement;
if ( 'undefined' !== typeof submenu ) {
let button = convertLinkToButton( menuItem );
setUpAria( submenu, button );
// bind event listener to button
button.addEventListener( 'click', toggleOnMenuClick );
menu.addEventListener( 'keyup', closeOnEscKey );
}
});
};
/**
* Why do this? See https://justmarkup.com/articles/2019-01-21-the-link-to-button-enhancement/
*/
function convertLinkToButton( menuItem ) {
const link = menuItem.getElementsByTagName( 'a' )[0],
linkHTML = link.innerHTML,
linkAtts = link.attributes,
button = document.createElement( 'button' );
if( null !== link ) {
// set button content and attributes
button.innerHTML = linkHTML.trim();
for( i = 0, len = linkAtts.length; i < len; i++ ) {
let attr = linkAtts[i];
if( 'href' !== attr.name ) {
button.setAttribute( attr.name, attr.value );
}
}
menuItem.replaceChild( button, link );
}
return button;
}
function setUpAria( submenu, button ) {
const submenuId = submenu.getAttribute( 'id' );
let id;
if( null === submenuId ) {
id = button.textContent.trim().replace(/\s+/g, '-').toLowerCase() + '-submenu';
} else {
id = menuItemId + '-submenu';
}
// set button ARIA
button.setAttribute( 'aria-controls', id );
button.setAttribute( 'aria-expanded', false );
// set submenu ARIA
submenu.setAttribute( 'id', id );
submenu.setAttribute( 'aria-hidden', true );
}
}
/* Create a ClickMenus object and initiate menu for any menu with .clicky-menu class */
document.addEventListener('DOMContentLoaded', function(){
const menus = document.querySelectorAll( '.clicky-menu' );
menus.forEach( menu => {
let clickyMenu = new ClickyMenus(menu);
clickyMenu.init();
});
});
}());
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.