<!-- 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">Now on <a href="https://github.com/mrwweb/clicky-menus">Github</a> and <a href="https://www.npmjs.com/package/clicky-menus">NPM!</a></p>
/**
* Clicky Menus v1.2.0
*/
/**
* Initial state, hidden off screen
*/
.clicky-menu ul {
position: absolute;
top: 100%;
left: 0;
visibility: hidden; /*[1]*/
}
.clicky-menu > li {
position: relative;
}
/**
* 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 {
min-height: 100vh;
background: linear-gradient(-37deg, teal, purple, orange) center/cover no-repeat;
font-family: 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;
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-family: inherit;
text-decoration: none;
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 {
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;
font-weight: 600;
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%;
margin-top: .25em;
padding: 0;
list-style: none;
background-color: #eee;
border-radius: 3px;
}
@media (min-width: 540px) {
.clicky-menu ul {
box-shadow: 2px 4px 4px rgba(0,0,0,.2);
transform: translateY(1em);
opacity: .5;
transition-property: transform opacity;
transition-duration: .1s;
}
}
/* Ensure no-js support works by covering the .25em margin-top gap between submenu and parent item with a pseudo-element that extends the "surface" of the submenu. If you don't care as much about no-js mode, you could also just set margin-top: 0 when .no-js is present. */
.clicky-menu.no-js ul::before {
position: absolute;
display: block;
content: "";
width: 100%;
height: .25em;
top: -.25em;
}
/* 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;
transform: translateY(0);
opacity: 1;
}
}
/* 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 {
position: fixed;
width: 90%;
left: 5%;
bottom: 10px;
text-align: center;
color: #fff;
}
.github a {
color: inherit;
}
View Compiled
/**
* Clicky Menus v1.2.0
*/
( function() {
'use strict';
const ClickyMenus = function( menu ) {
// DOM element(s)
const container = menu.parentElement;
let currentMenuItem,
i,
len;
this.init = function() {
menuSetup();
document.addEventListener( 'click', closeIfClickOutsideMenu );
// custom event to allow outside scripts to close submenus
menu.addEventListener( 'clickyMenusClose', closeOpenSubmenu );
};
/*===================================================
= 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 closeIfClickOutsideMenu( e ) {
if ( currentMenuItem && ! e.target.closest( '#' + container.id ) ) {
toggleSubmenu( currentMenuItem );
}
}
function closeOpenSubmenu() {
if( currentMenuItem ) {
toggleSubmenu( currentMenuItem );
}
}
/*===========================================================
= Modify Menu Markup & Bind Listeners =
=============================================================*/
function menuSetup() {
menu.classList.remove( 'no-js' );
const submenuSelector = 'clickySubmenuSelector' in menu.dataset ? menu.dataset.clickySubmenuSelector : 'ul';
menu.querySelectorAll( submenuSelector ).forEach( ( submenu ) => {
const menuItem = submenu.parentElement;
if ( 'undefined' !== typeof submenu ) {
const 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/
*
* @param {HTMLElement} menuItem An element representing a link to be converted to a button
*/
function convertLinkToButton( menuItem ) {
const link = menuItem.getElementsByTagName( 'a' )[ 0 ],
linkHTML = link.innerHTML,
linkAtts = link.attributes,
button = document.createElement( 'button' );
if ( null !== link ) {
// copy button attributes and content from link
button.innerHTML = linkHTML.trim();
for ( i = 0, len = linkAtts.length; i < len; i++ ) {
const 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 = submenuId + '-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 ) => {
const clickyMenu = new ClickyMenus( menu );
clickyMenu.init();
} );
} );
}() );
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.