This JavaScript code snippet helps you to create a responsive drop down menu with submenu. It uses HTML5 nav element to arrange the list of links into a horizontal dropdown menu. The dropdown opens with a popup animation on the click event and can be closed by clicking anywhere on the webpage.
How to Create a Responsive Drop Down Menu With Submenu
1. First of all, load the Normalize CSS and Bootstrap 5 CSS (optional for layout) by adding the following CDN links into the head tag of your HTML document.
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css"> <link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.0.1/css/bootstrap.min.css'>
2. After that, create a nav element with a unique id, define its role attribute “navigation”, and place your logo and list of links inside it. Wrap the nav element into a div element and define its class name “nav-wrapper”. The following is the complete HTML structure for the drop down menu with submenu:
<div id="nav-wrapper"> <nav id="ddmenu-extra" role="navigation"> <a id="hamburger" role="button" aria-pressed="false" aria-expanded="false" aria-haspopup="true" aria-label="Open Menu" href="#"> <span class="title" aria-hidden="true">Menu</span> <span class="line"></span> <span class="line"></span> <span class="line"></span> </a> <a class="logo ir" title="Click to go to Placeholder Company home page." href="./"> <h1>Welcome to the Placeholder Company Website</h1> </a> </nav> <nav id="ddmenu-main" class="ddmenu" role="navigation"> <a id="hamburger-close" role="button" aria-label="Close Menu" href="#"> <span class="title" aria-hidden="true">Close</span> <span class="line one"></span> <span class="line two"></span> </a> <ul> <li><a href="./">Home</a></li> <li><a href="#about">About</a></li> <li> <a href="#">Products</a> <ul> <li> <a href="#">For the Home</a> <ul> <li><a href="#">Bath</a></li> <li> <a href="#">Kitchen</a> <ul> <li><a href="#">Widgets</a></li> <li><a href="#">Cutlery</a></li> <li><a href="#">Thermometers</a></li> <li><a href="#">Scales</a></li> <li><a href="#">Storage</a></li> </ul> </li> <li><a href="#">Exterior</a></li> <li><a href="#">Other</a></li> </ul> </li> <li> <a href="#">Corporate</a> <ul> <li><a href="#">B2B</a></li> <li><a href="#">Turnkey</a></li> <li><a href="#">Franchises</a></li> <li><a href="#">Other</a></li> </ul> </li> <li><a href="#">Non Profits</a></li> </ul> </li> <li> <a href="#">Services</a> <ul> <li><a href="#">Cleanup</a></li> <li><a href="#">Washup</a></li> <li><a href="#">Tidying</a></li> </ul> </li> <li><a href="#faq">F.A.Q.</a></li> <li><a href="#contact">Contact</a></li> </ul> </nav> </div> <!-- .nav-wrapper -->
3. Now, style the drop down menu using the following CSS styles. You can set the custom values for the CSS properties in order to customize the navigation menu.
@charset "UTF-8"; /* THIS IS THE GENERATED CSS FILE */ /* begin general css */ html { font-size: 62.5%; --scroll-behavior: smooth; scroll-behavior: smooth; } .cd__main{ display: block !important; } body { position: relative; font-family: "MYRIADPROREGULAR", sans-serif !important; line-height: 1.5; -webkit-hyphens: auto; -ms-hyphens: auto; hyphens: auto; color: #000; background-size: cover; } @media only screen and (min-width: 0) and (max-width: 575px) { body { font-size: 1.44rem; } } @media only screen and (min-width: 576px) { body { font-size: 1.6rem; } } * { -webkit-overflow-scrolling: touch; box-sizing: border-box; } .i { font-style: italic; } .ni { font-style: normal !important; } .b { font-weight: bold; } .bni { font-weight: normal !important; } .ir, .ir:hover, .ir:focus, .ir:focus-within { font: 0/0 a; text-shadow: none; color: transparent; background-color: transparent; border: 0; } .rt { float: right; } .lt { float: left; } .cb { clear: both !important; } .all-caps { text-transform: uppercase; } .no-caps { text-transform: lowercase; } .first-caps { text-transform: capitalize; } .npr { padding-right: 0px !important; } .npl { padding-left: 0px !important; } .nmr { margin-right: 0px !important; } .nml { margin-left: 0px !important; } .ie9 * { /* http://www.colorzilla.com/gradient-editor/ */ filter: none; } a:active { outline: none !important; } :focus:not(:focus-visible) { outline: none !important; outline-color: transparent !important; } a#top { position: absolute; top: -10px; left: 0px; } a.std { color: #00a1e1; text-decoration: underline; } a.std:visited, a.std:active { color: #00a1e1; } a.std:hover { color: #ea7207; font-style: italic; text-decoration: none !important; } hr { height: 1px; width: 100%; background-color: #20145f; } /* forces default iOS telephone link styling back to original styling */ a[href^=tel] { color: inherit; text-decoration: none; } a#skip-to-main-content { position: fixed; z-index: 10000; display: inline-block; left: 50%; top: 0; transform: translate(-50%, -100vh); background-color: rgba(0, 0, 0, 0.75); color: white; padding: 20px; opacity: 0; transition: opacity ease-in-out 0.5s, transform ease-in-out 0.2s, color ease-in-out 0.5s, outline-offset ease-in-out 0.5s; cursor: pointer; text-decoration: none; text-align: center; box-shadow: 0px 5px 7px rgba(0, 0, 0, 0.5); outline-offset: -40px; } a#skip-to-main-content:hover { color: cyan; text-decoration: underline; } a#skip-to-main-content:focus-visible { transform: translate(-50%, 0vh); opacity: 1; outline-offset: -4px; outline-color: white; outline-style: dashed; } /* begin custom scrollbar */ /* width */ ::-webkit-scrollbar { width: 8px; } /* Track */ ::-webkit-scrollbar-track { background-color: rgba(0, 0, 0, 0.05); } ::-webkit-scrollbar-track:hover { background-color: WhiteSmoke; } /* Handle */ ::-webkit-scrollbar-thumb { background-color: rgba(0, 0, 0, 0.5); border-radius: 4px; border: 2px solid CornSilk; } /* Handle on hover */ ::-webkit-scrollbar-thumb:hover { background: DarkMagenta; border: none; } * { scrollbar-width: thin; scrollbar-color: DarkMagenta rgba(0, 0, 0, 0.1); } /* end custom scrollbar */ /* begin readout */ div#readout { display: none; position: fixed; z-index: 10000; bottom: 0px; left: 0px; width: 200px; height: 300px; color: #fff; background-color: rgba(0, 0, 0, 0.5); overflow: auto; font-size: 1em; } div#readout::before { color: yellow; display: block; clear: both; } @media only screen and (min-width: 1400px) { div#readout::before { content: "350px / XXL"; } } @media only screen and (min-width: 1200px) and (max-width: 1399px) { div#readout::before { content: "300px / XL"; } } @media only screen and (min-width: 992px) and (max-width: 1199px) { div#readout::before { content: "250px / L"; } } @media only screen and (min-width: 768px) and (max-width: 991px) { div#readout::before { content: "200px / M"; } } @media only screen and (min-width: 576px) and (max-width: 767px) { div#readout::before { content: "150px / S"; } } @media only screen and (min-width: 0) and (max-width: 575px) { div#readout::before { content: "100px / XS"; } } @media only screen and (max-width: 450px) { div#readout::before { content: "50px / 450"; } } /* end readout */ div#rangeBox { position: fixed; overflow: hidden; top: 100px; left: -20px; width: 20px; min-height: 10px; color: #fff; background-color: rgba(0, 0, 0, 0.5); z-index: 10000; } @media only screen and (min-width: 1400px) { div#rangeBox { height: 350px; } } @media only screen and (min-width: 1200px) and (max-width: 1399px) { div#rangeBox { height: 300px; } } @media only screen and (min-width: 992px) and (max-width: 1199px) { div#rangeBox { height: 250px; } } @media only screen and (min-width: 768px) and (max-width: 991px) { div#rangeBox { height: 200px; } } @media only screen and (min-width: 576px) and (max-width: 767px) { div#rangeBox { height: 150px; } } @media only screen and (min-width: 0) and (max-width: 575px) { div#rangeBox { height: 100px; } } @media only screen and (max-width: 450px) { div#rangeBox { height: 50px; } } /* end general css */ /* begin site styles */ body { background-color: CornSilk; overflow-x: hidden; } body.mobile-menu-opened, html.mobile-menu-opened { overflow: hidden; } @media only screen and (min-width: 768px) { header { position: relative; transform: translate(0%, 0%); box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0); transition: box-shadow 0.4s linear !important; } } @media only screen and (min-width: 768px) { header.sticky { background-color: CornSilk; width: 100%; display: flex; justify-content: center; } } @media only screen and (min-width: 1200px) { header.sticky { margin: 0 auto !important; } } @media only screen and (min-width: 992px) and (max-width: 1199px) { header.sticky { width: 100% !important; max-width: 100% !important; } } @media only screen and (min-width: 768px) and (max-width: 991px) { header.sticky { width: 100% !important; max-width: 100% !important; } } @media only screen and (min-width: 768px) { header.sticky { position: relative; transform: translate(0%, calc(-100% - 10px)); transition: transform 0.4s ease-in-out !important; } } @media only screen and (min-width: 768px) { header.sticky div#nav-wrapper { margin: 0px !important; } } header.sticky nav#ddmenu-extra { margin: 2px !important; } @media only screen and (min-width: 768px) { header.sticky a.logo { width: 167px !important; height: 35px !important; margin: 0 !important; } } @media only screen and (min-width: 768px) { header.sticky.in::before { content: ""; display: block; position: absolute; z-index: -1; width: 100%; height: 100%; } } @media only screen and (min-width: 768px) { header.sticky.in { position: fixed; transform: translate(0%, 0%); box-shadow: 0px 2px 2px 1px rgba(0, 0, 0, 0.1); } } @media only screen and (min-width: 768px) { header.sticky.in.out { transform: translate(0%, calc(-100% - 10px)); } } @media only screen and (min-width: 768px) { header.sticky.in.out::before { opacity: 0; } } @media only screen and (max-width: 768px) { header + main > .container { padding-top: 0px !important; } } @media only screen and (min-width: 576px) { main h2 { text-align: left; } } @media only screen and (max-width: 576px) { main h2 { text-align: center; } } main img.example { width: 100%; height: auto; display: block; } @media only screen and (min-width: 768px) { main img.example { margin: 10px; margin-left: 0px; float: left; } } @media only screen and (min-width: 576px) { main img.example { max-width: 429px; } } @media only screen and (min-width: 576px) and (max-width: 767px) { main img.example { margin: 10px; float: none; margin: 0 auto; } } @media only screen and (max-width: 576px) { main img.example { max-width: 320px; margin: 0px; float: none; margin: 0 auto; } } a { color: DarkMagenta; transition: all ease 0.5s; } a:hover { color: Teal; } a:focus { outline-color: black; outline-style: dashed; outline-offset: 2px; outline-width: 1px; } h2, h3, h4, h5, h6 { font-weight: bold; } :root { --ddbaseHeight: 40px; --ddbaseWidth: 150px; --ddnavBGColor: transparent; --ddnavMobileBGColor: MediumSeaGreen; --ddnavMobileBGGradientStart: MediumSeaGreen; --ddnavMobileBGGradientMid: LightGreen; --ddnavMobileBGGradientEnd: CornSilk; --ddanchorBGHover: MediumBlue; --ddanchorBGSelected: MediumBlue; --ddfirstLvlBG: DarkMagenta; --ddsecondLvlBG: DodgerBlue; --ddthirdLvlBG: SteelBlue; --ddfourthLvlBG: DarkOliveGreen; --ddanchorTextColor: white; --ddanchorTextHoverColor: CornSilk; --ddanchorTxtSelectedColor: CornSilk; --ddoutlineColor: white; --ddoutlineColorLogo: black; --ddoutlineOffset: -4px; } div#nav-wrapper { position: relative; display: flex; flex-wrap: wrap; align-items: center; width: 100%; min-height: var(--ddbaseHeight); margin-top: 15px; margin-bottom: 15px; transition: margin 0.4s ease-in-out; } @media only screen and (min-width: 1200px) { div#nav-wrapper { justify-content: flex-end; } } @media only screen and (min-width: 992px) and (max-width: 1199px) { div#nav-wrapper { justify-content: center; } } nav#ddmenu-extra { position: relative; display: flex; align-self: center; } @media only screen and (min-width: 1200px) { nav#ddmenu-extra { position: absolute !important; left: 0px; } } @media only screen and (max-width: 1200px) { nav#ddmenu-extra { width: 100%; margin-bottom: 10px; } } @media only screen and (min-width: 992px) and (max-width: 1199px) { nav#ddmenu-extra { justify-content: center; } } @media only screen and (min-width: 768px) and (max-width: 991px) { nav#ddmenu-extra { justify-content: center; } } @media only screen and (max-width: 768px) { nav#ddmenu-extra { flex-direction: row-reverse; justify-content: flex-start; } } nav#ddmenu-extra a#hamburger { position: relative; display: block; overflow: hidden; padding: 0; font-size: 0rem; text-align: right; border: none; background-color: transparent; } @media only screen and (min-width: 768px) { nav#ddmenu-extra a#hamburger { display: none; } } @media only screen and (max-width: 768px) { nav#ddmenu-extra a#hamburger { align-self: center; width: 40px; height: 40px; margin-left: auto; } } nav#ddmenu-extra a#hamburger:focus-visible { outline-offset: calc(var(--ddoutlineOffset) * -1); outline-color: #000; } .desktop nav#ddmenu-extra a#hamburger:hover span.title { color: DarkMagenta; } .desktop nav#ddmenu-extra a#hamburger:hover span.line { border-color: DarkMagenta; } .desktop nav#ddmenu-extra a#hamburger:hover span.line::after { transform: translate(-100%, 0); } nav#ddmenu-extra a#hamburger span.title { display: inline-block; position: absolute; bottom: 0px; width: 100%; font-size: 1rem; text-align: center; color: #000; transform: translate(-50%, 2px); transition: transform 0.25s ease-in-out, color 0.5s ease-in-out; } nav#ddmenu-extra a#hamburger span.line { display: inline-block; position: relative; top: 4px; height: 6px; margin-bottom: 3px; border-top: 1px solid #000; transition: border-color 0.25s ease-in-out; } nav#ddmenu-extra a#hamburger span.line:nth-of-type(2) { width: 50%; } nav#ddmenu-extra a#hamburger span.line:nth-of-type(3) { width: 75%; } nav#ddmenu-extra a#hamburger span.line:nth-of-type(3)::after { transition-delay: 0.25s; } nav#ddmenu-extra a#hamburger span.line:nth-of-type(4) { width: 100%; margin-bottom: 0px; } nav#ddmenu-extra a#hamburger span.line:nth-of-type(4)::after { transition-delay: 0.5s; } nav#ddmenu-extra a#hamburger span.line::after { content: ""; display: inline-block; position: absolute; width: 100%; height: 100%; background-color: DarkMagenta; transition: transform 0.5s ease-in-out; } nav#ddmenu-extra a.logo { display: block; position: relative; z-index: 1; float: left; width: 250px; height: 52px; overflow: hidden; line-height: 0; background-image: url("https://placeholder.com/wp-content/uploads/2018/10/placeholder.com-logo1.png"); background-repeat: no-repeat; background-size: 100% auto; background-position: top left; transition: outline-offset 0.4s ease-in-out, width 0.4s ease-in-out, height 0.4s ease-in-out; } nav#ddmenu-extra a.logo::before, nav#ddmenu-extra a.logo::after { display: none !important; } nav#ddmenu-extra a.logo:focus-visible { outline-offset: calc(var(--ddoutlineOffset) * -1); outline-color: var(--ddoutlineColorLogo); } @media only screen and (max-width: 768px) { nav#ddmenu-main { position: fixed; z-index: 9000; display: flex; justify-content: center; align-items: flex-start; visibility: hidden; opacity: 0; overflow: auto; width: 100%; height: 100%; top: 0; bottom: 0; left: 0; right: 0; padding: 15px; background-color: var(--ddnavMobileBGColor); background: linear-gradient(180deg, var(--ddnavMobileBGGradientStart) 0%, var(--ddnavMobileBGGradientStart) 35%, var(--ddnavMobileBGGradientMid) 80%, var(--ddnavMobileBGGradientEnd) 100%); background-position: center top; background-repeat: no-repeat; background-size: cover; transform: translate(0px, -100%); } } @media only screen and (max-width: 768px) and (prefers-reduced-motion: reduce) { nav#ddmenu-main { -webkit-animation: none; animation: none; transition: none; } } @media only screen and (max-width: 768px) and (prefers-reduced-motion: no-preference) { nav#ddmenu-main { transition: visibility 0s ease-in-out 0.5s, opacity 0.5s ease-in-out 0s, transform 0.5s ease-in-out 0s; } } @media only screen and (max-width: 768px) { nav#ddmenu-main.opened { opacity: 1; visibility: visible; transition-delay: 0s, 0s, 0s; transform: translate(0px, 0px); } } nav#ddmenu-main.opened > ul::before { width: 100%; height: 100%; visibility: visible; opacity: 1; box-shadow: 0px 2px 2px 1px rgba(0, 0, 0, 0.25); transition: visibility 0s ease-in-out 1.5s, opacity 0.75s ease-in-out 1.5s, transform 0.75s ease-in-out 1.5s; } nav#ddmenu-main.opened > ul > li { opacity: 1; transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg) translate(0px, 0px) scale(1); } @media only screen and (min-width: 768px) { nav#ddmenu-main a#hamburger-close { display: none; } } @media only screen and (max-width: 768px) { nav#ddmenu-main a#hamburger-close { position: absolute; display: flex; justify-content: center; align-items: center; z-index: 10; top: 10px; right: 10px; width: 40px; height: 40px; font-size: 0rem; text-decoration: none; color: #000; } .desktop nav#ddmenu-main a#hamburger-close:hover span.title { color: DarkMagenta; } .desktop nav#ddmenu-main a#hamburger-close:hover span.line { background-color: DarkMagenta; } .desktop nav#ddmenu-main a#hamburger-close:hover span.line.one { transform: rotate(225deg); } .desktop nav#ddmenu-main a#hamburger-close:hover span.line.two { transform: rotate(135deg); } } @media only screen and (max-width: 768px) { nav#ddmenu-main a#hamburger-close span.title { position: absolute; bottom: 0px; font-size: 1rem; transition: color 0.4s ease-in-out; } } @media only screen and (max-width: 768px) { nav#ddmenu-main a#hamburger-close span.line { position: absolute; top: 26%; left: 27%; display: block; width: 20px; height: 4px; background-color: #000; transform-origin: 50% 50%; } } @media only screen and (max-width: 768px) and (prefers-reduced-motion: reduce) { nav#ddmenu-main a#hamburger-close span.line { -webkit-animation: none; animation: none; transition: none; } } @media only screen and (max-width: 768px) and (prefers-reduced-motion: no-preference) { nav#ddmenu-main a#hamburger-close span.line { transition: background-color 0.4s ease-in-out, transform 0.4s ease-in-out; } } @media only screen and (max-width: 768px) { nav#ddmenu-main a#hamburger-close span.line.one { transform: rotate(45deg); } } @media only screen and (max-width: 768px) { nav#ddmenu-main a#hamburger-close span.line.two { transform: rotate(-45deg); } } @media only screen and (min-width: 768px) { nav.ddmenu { display: flex; align-self: center; align-items: center; position: relative; min-height: var(--ddbaseHeight); background-color: var(--ddnavBGColor); } } @media only screen and (min-width: 1200px) { nav.ddmenu { justify-content: flex-end; width: calc(100% - 250px); } } @media only screen and (max-width: 1200px) { nav.ddmenu { width: 100%; } } @media only screen and (min-width: 992px) and (max-width: 1199px) { nav.ddmenu { justify-content: center; } } @media only screen and (min-width: 768px) and (max-width: 991px) { nav.ddmenu { justify-content: center; } } nav.ddmenu li a { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; min-height: var(--ddbaseHeight); padding-left: 10px; padding-right: 10px; padding-top: 10px; padding-bottom: 10px; font-family: verdana, sans-serif; -webkit-hyphens: auto; -ms-hyphens: auto; hyphens: auto; text-decoration: none; line-height: 1.25; color: var(--ddanchorTextColor); text-align: center; outline-offset: calc(max(var(--ddbaseHeight) * -1, var(--ddbaseWidth) * -1)); outline: 1px dashed transparent; } @media (prefers-reduced-motion: reduce) { nav.ddmenu li a { -webkit-animation: none; animation: none; transition: none; } } @media (prefers-reduced-motion: no-preference) { nav.ddmenu li a { transition: color ease-in-out 0.5s, background-color ease-in-out 0.5s, outline-offset ease-in-out 0.25s, outline-color ease-in-out 0.25s; } } .desktop nav.ddmenu li a:hover { background-color: var(--ddanchorBGHover); color: var(--ddanchorTextHoverColor); } nav.ddmenu li a:focus-visible { outline-offset: var(--ddoutlineOffset); outline: 1px dashed var(--ddoutlineColor); background-color: var(--ddanchorBGSelected); color: var(--ddanchorTxtSelectedColor); } nav.ddmenu li a.hasSub::before, nav.ddmenu li a.hasSub::after { content: "▼"; font-size: 0.75em; margin-left: 0.25em; } @media (prefers-reduced-motion: reduce) { nav.ddmenu li a.hasSub::before, nav.ddmenu li a.hasSub::after { -webkit-animation: none; animation: none; transition: none; } } @media (prefers-reduced-motion: no-preference) { nav.ddmenu li a.hasSub::before, nav.ddmenu li a.hasSub::after { transition: transform ease-in-out 0.25s, color ease-in-out 0.25s; } } nav.ddmenu li a.hasSub::before { display: none; } @media only screen and (max-width: 768px) { nav.ddmenu li a.hasSub::before { display: none !important; } } @media only screen and (max-width: 768px) { nav.ddmenu li a.hasSub::after { display: block !important; } } nav.ddmenu li a.hasSub.opened::after { transform: rotate(180deg); } nav.ddmenu ul, nav.ddmenu li { margin: 0; padding: 0; list-style: none; } nav.ddmenu > ul { position: relative; } @media only screen and (min-width: 768px) { nav.ddmenu > ul { display: flex; flex-wrap: wrap; width: 100%; height: 100%; box-shadow: none !important; transition: all 0s; } } nav.ddmenu > ul ul.shiftLeft a.hasSub::after { display: none; } nav.ddmenu > ul ul.shiftLeft a.hasSub::before { display: block; margin-left: 0 !important; margin-right: 0.25em; transform: rotate(90deg); } nav.ddmenu > ul ul.shiftLeft a.hasSub.opened::before { transform: rotate(-90deg); } @media only screen and (min-width: 768px) { nav.ddmenu > ul ul.shiftLeft > li > ul { left: calc(-100% - 5px) !important; } } @media only screen and (min-width: 768px) { nav.ddmenu > ul ul.shiftLeft > li > ul::before { left: calc(100% - 6px); transform: rotate(135deg); } } @media only screen and (min-width: 1200px) { nav.ddmenu > ul { justify-content: flex-end; } } @media only screen and (min-width: 992px) and (max-width: 1199px) { nav.ddmenu > ul { justify-content: center; } } @media only screen and (min-width: 768px) and (max-width: 991px) { nav.ddmenu > ul { justify-content: center; } } @media only screen and (max-width: 768px) { nav.ddmenu > ul { justify-content: center; width: calc(100% - 100px); min-width: calc(var(--ddbaseWidth) * 0.9); max-width: calc(var(--ddbaseWidth) * 2.5); } nav.ddmenu > ul::before { content: ""; position: absolute; visibillity: hidden; opacity: 0; width: 0; height: 0; box-shadow: 0px 0px 0px 0px rgba(0, 0, 0, 0.25); } } @media only screen and (max-width: 768px) and (prefers-reduced-motion: reduce) { nav.ddmenu > ul::before { -webkit-animation: none; animation: none; transition: none; } } @media only screen and (min-width: 768px) { nav.ddmenu > ul > li:first-of-type { display: none; } } @media only screen and (max-width: 768px) { nav.ddmenu > ul > li:first-of-type { display: block; } } nav.ddmenu > ul li { position: relative; display: block; } @media only screen and (min-width: 992px) { nav.ddmenu > ul li { width: var(--ddbaseWidth); } } @media only screen and (min-width: 768px) and (max-width: 991px) { nav.ddmenu > ul li { width: calc(0.9 * var(--ddbaseWidth)); } } @media only screen and (max-width: 768px) { nav.ddmenu > ul li { width: 100%; } } @media (prefers-reduced-motion: reduce) { nav.ddmenu > ul li ul { -webkit-animation: none; animation: none; transition: none; } } @media only screen and (min-width: 768px) { nav.ddmenu > ul li ul { visibility: hidden; opacity: 0; transform: scale(0.5); transform-origin: 50% calc(50% - var(--ddbaseHeight)); } } @media only screen and (min-width: 768px) and (prefers-reduced-motion: no-preference) { nav.ddmenu > ul li ul { transition: visibility 0s ease-in-out 0.4s, opacity 0.4s ease-in-out 0s, transform 0.4s ease-in-out 0s; } } @media only screen and (max-width: 768px) { nav.ddmenu > ul li ul { overflow: hidden; max-height: 0px; opacity: 0; visibility: hidden; } } @media only screen and (max-width: 768px) and (prefers-reduced-motion: no-preference) { nav.ddmenu > ul li ul { transition: visibility 0s ease-in-out 0.4s, opacity 0.4s ease-in-out 0s, max-height 0.4s ease-in-out 0s; } } nav.ddmenu > ul li ul::before { position: absolute; display: block; content: ""; width: 10px; height: 10px; -webkit-clip-path: polygon(0% 0%, 100% 0px, 0px 100%); clip-path: polygon(0% 0%, 100% 0px, 0px 100%); } nav.ddmenu > ul > li { background-color: var(--ddfirstLvlBG); } @media only screen and (max-width: 768px) { nav.ddmenu > ul > li { opacity: 0; transform-origin: center bottom; transform: rotateX(-90deg) rotateY(0deg) rotateZ(-20deg) translate(0px, -200%) scale(0.05); } } @media only screen and (max-width: 768px) and (prefers-reduced-motion: reduce) { nav.ddmenu > ul > li { -webkit-animation: none; animation: none; transition: none; } } @media only screen and (max-width: 768px) and (prefers-reduced-motion: no-preference) { nav.ddmenu > ul > li { transition: opacity 1s ease-in-out, transform 0.75s cubic-bezier(0.68, -0.55, 0.265, 1.55); } nav.ddmenu > ul > li:nth-of-type(1) { transition-delay: 0.9s, 0.9s; } nav.ddmenu > ul > li:nth-of-type(2) { transition-delay: 0.8s, 0.8s; } nav.ddmenu > ul > li:nth-of-type(3) { transition-delay: 0.7s, 0.7s; } nav.ddmenu > ul > li:nth-of-type(4) { transition-delay: 0.6s, 0.6s; } nav.ddmenu > ul > li:nth-of-type(5) { transition-delay: 0.5s, 0.5s; } nav.ddmenu > ul > li:nth-of-type(6) { transition-delay: 0.4s, 0.4s; } nav.ddmenu > ul > li:nth-of-type(7) { transition-delay: 0.3s, 0.3s; } nav.ddmenu > ul > li:nth-of-type(8) { transition-delay: 0.2s, 0.2s; } nav.ddmenu > ul > li:nth-of-type(9) { transition-delay: 0.1s, 0.1s; } nav.ddmenu > ul > li:nth-of-type(10) { transition-delay: 0s, 0s; } } @media only screen and (min-width: 768px) { nav.ddmenu > ul > li > ul { position: absolute; z-index: 1; top: calc(var(--ddbaseHeight) + 5px); } } nav.ddmenu > ul > li > ul > li { background-color: var(--ddsecondLvlBG); } @media only screen and (min-width: 768px) { nav.ddmenu > ul > li > ul a.hasSub::after { transform: rotate(-90deg); } } @media only screen and (min-width: 768px) { nav.ddmenu > ul > li > ul a.hasSub.opened::after { transform: rotate(90deg); } } nav.ddmenu > ul > li > ul::before { background-color: var(--ddsecondLvlBG); margin-left: calc(50% - 5px); transform: rotate(45deg); } @media only screen and (min-width: 768px) { nav.ddmenu > ul > li > ul::before { top: -3px; left: 0px; } } @media only screen and (max-width: 768px) { nav.ddmenu > ul > li > ul::before { top: calc(var(--ddbaseHeight) - 3px) !important; } } @media only screen and (min-width: 768px) { nav.ddmenu > ul > li > ul > li ul { position: absolute; top: 0px; left: calc(100% + 5px); } } nav.ddmenu > ul > li > ul > li ul::before { background-color: var(--ddthirdLvlBG); } @media only screen and (min-width: 768px) { nav.ddmenu > ul > li > ul > li ul::before { top: calc(var(--ddbaseHeight) / 2 - 3px); left: -3px; transform: rotate(-45deg); } } @media only screen and (max-width: 768px) { nav.ddmenu > ul > li > ul > li ul::before { top: calc(var(--ddbaseHeight) - 3px) !important; left: calc(50% - 3px); transform: rotate(45deg); } } nav.ddmenu > ul > li > ul > li > ul > li { background-color: var(--ddthirdLvlBG); } nav.ddmenu > ul > li > ul > li > ul > li > ul::before { background-color: var(--ddfourthLvlBG) !important; } nav.ddmenu > ul > li > ul > li > ul > li > ul > li { background-color: var(--ddfourthLvlBG); } .ddmenu:not(.js-enabled) li:hover > ul, .ddmenu ul.selected { transition-delay: 0s, 0s, 0s; } @media only screen and (min-width: 768px) { .ddmenu:not(.js-enabled) li:hover > ul, .ddmenu ul.selected { visibility: visible; opacity: 1; transform: scale(1); transform-origin: 50% 50%; box-shadow: 0px 2px 2px 1px rgba(0, 0, 0, 0.25); } } @media only screen and (max-width: 768px) { .ddmenu:not(.js-enabled) li:hover > ul, .ddmenu ul.selected { visibility: visible; opacity: 1; max-height: 500px; overflow: initial; } } @-webkit-keyframes ddClipIn { 0% { -webkit-clip-path: polygon(-5px -5px, calc(100% + 5px) -5px, calc(100% + 5px) -5px, -5px -5px); clip-path: polygon(-5px -5px, calc(100% + 5px) -5px, calc(100% + 5px) -5px, -5px -5px); } 100% { -webkit-clip-path: polygon(-5px -5px, calc(100% + 5px) -5px, calc(100% + 5px) calc(100% + 5px), -5px 100%); clip-path: polygon(-5px -5px, calc(100% + 5px) -5px, calc(100% + 5px) calc(100% + 5px), -5px 100%); } } @keyframes ddClipIn { 0% { -webkit-clip-path: polygon(-5px -5px, calc(100% + 5px) -5px, calc(100% + 5px) -5px, -5px -5px); clip-path: polygon(-5px -5px, calc(100% + 5px) -5px, calc(100% + 5px) -5px, -5px -5px); } 100% { -webkit-clip-path: polygon(-5px -5px, calc(100% + 5px) -5px, calc(100% + 5px) calc(100% + 5px), -5px 100%); clip-path: polygon(-5px -5px, calc(100% + 5px) -5px, calc(100% + 5px) calc(100% + 5px), -5px 100%); } } @-webkit-keyframes ddClipOut { 0% { -webkit-clip-path: polygon(-5px -5px, calc(100% + 5px) -5px, calc(100% + 5px) calc(100% + 5px), -5px 100%); clip-path: polygon(-5px -5px, calc(100% + 5px) -5px, calc(100% + 5px) calc(100% + 5px), -5px 100%); } 100% { -webkit-clip-path: polygon(-5px -5px, calc(100% + 5px) -5px, calc(100% + 5px) -5px, -5px -5px); clip-path: polygon(-5px -5px, calc(100% + 5px) -5px, calc(100% + 5px) -5px, -5px -5px); } } @keyframes ddClipOut { 0% { -webkit-clip-path: polygon(-5px -5px, calc(100% + 5px) -5px, calc(100% + 5px) calc(100% + 5px), -5px 100%); clip-path: polygon(-5px -5px, calc(100% + 5px) -5px, calc(100% + 5px) calc(100% + 5px), -5px 100%); } 100% { -webkit-clip-path: polygon(-5px -5px, calc(100% + 5px) -5px, calc(100% + 5px) -5px, -5px -5px); clip-path: polygon(-5px -5px, calc(100% + 5px) -5px, calc(100% + 5px) -5px, -5px -5px); } } /* end site styles */
Load the following scripts before closing the body tag:
<script src='https://cdnjs.cloudflare.com/ajax/libs/headjs/1.0.3/head.min.js'></script> <script src='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.0.1/js/bootstrap.min.js'></script> <script src='https://unpkg.com/smoothscroll-polyfill/dist/smoothscroll.min.js'></script> <script src='https://unpkg.com/smoothscroll-anchor-polyfill'></script>
4. Finally, add the following JavaScript function before closing the body tag and done.
/* This is a WIP. Features: 1. Click based dropdown menu. 2. Plain vanilla JavaScript. 3. Mobile menu enabled, with opening animation. 4. Submenus account for right edge of browser. 5. Submenus have arrow indicators that point appropriately. 6. Scrolling disabled when mobile menu is opened. 7. Menu is directionally scroll-aware for MQ(M) or larger. Items to improve- A. add js disabled styles for mobile view, MQ(S) and smaller */ (function () { //begin Self-Executing Anonymous Function "use strict"; //equivalent to jQuery document ready event document.addEventListener("DOMContentLoaded", domContentLoaded); function domContentLoaded() { //setup keys to trigger a click on skip to main content link keysTriggerClick(document.getElementById("skip-to-main-content")); //overall drop down menu let ddmenuMain = document.getElementById("ddmenu-main"); //setup main navigation dropdown initDdmenu(ddmenuMain); } //end fxn domContentLoaded function getOffset(el) { //https://stackoverflow.com/questions/442404/retrieve-the-position-x-y-of-an-html-element-relative-to-the-browser-window#11396681 //usage: xCoord = getOffset( document.getElementById('yourElId') ).left let _x = 0; let _y = 0; while (el && !isNaN(el.offsetLeft) && !isNaN(el.offsetTop)) { _x += el.offsetLeft - el.scrollLeft; _y += el.offsetTop - el.scrollTop; el = el.offsetParent; } return { top: _y, left: _x }; } //end fxn getOffset function initDdmenu(ddMenu) { //begin setup hamburger menu anchors let hamburger = document.getElementById("hamburger"); let hamburgerClose = document.getElementById("hamburger-close"); let body = document.getElementsByTagName("body")[0]; let html = document.getElementsByTagName("html")[0]; let skipToMain = document.getElementById("skip-to-main-content"); keysTriggerClick(hamburger); keysTriggerClick(hamburgerClose, ["Escape"]); //allow default keys and escape key to trigger click hamburger.addEventListener("click", function (e) { e.preventDefault(); if (!ddMenu.classList.contains("opened")) { ddMenu.classList.add("opened"); this.setAttribute("aria-pressed", "true"); this.setAttribute("aria-expanded", "true"); //set timeout to be no smaller than 50 to allow time for visibility of menu to change window.setTimeout(function () { hamburgerClose.focus(); }, 50); //give class to prevent scrolling while mobile menu is open body.classList.add("mobile-menu-opened"); html.classList.add("mobile-menu-opened"); skipToMain.style.visibility = "hidden"; } //end if }); //end hamburger click hamburgerClose.addEventListener("click", function (e) { e.preventDefault(); if (ddMenu.classList.contains("opened")) { ddMenu.classList.remove("opened"); hamburger.setAttribute("aria-pressed", "false"); hamburger.setAttribute("aria-expanded", "false"); //set timeout to be no smaller than 50 to allow time for visibility of menu to change window.setTimeout(function () { hamburger.focus(); }, 50); body.classList.remove("mobile-menu-opened"); html.classList.remove("mobile-menu-opened"); skipToMain.style.visibility = "visible"; } //end if }); //end hamburgerClose click //variables for window events let windowWidth = window.outerWidth; let windowHeight = window.outerHeight; let resizeTimer; //for window resize event setTimeout let resizeDelay = 20; //throttling delay for window resize event in milliseconds let scrollTimer; //for window scroll event setTimeout let scrollDelay; //throttling delay for window scroll event in milliseconds let yStart = window.pageYOffset; //initial scroll position let rangeBox = document.getElementById("rangeBox"); //el that changes height based on different MQs let rangeBoxHeight; //default heights for rangeBox at various media queries is within its definition in the stylesheet if (html.classList.contains("ff") && html.classList.contains("desktop")) { scrollDelay = 50; //ff for desktop has a problem with really low scrollDelay values } else if ( html.classList.contains("safari") && html.classList.contains("mobile") ) { scrollDelay = 0; //safari under ios seems to require this } else { scrollDelay = 20; } window.addEventListener("load", function () { // get initial window dimensions for resize event check windowWidth = window.outerWidth; windowHeight = window.outerHeight; }); //end window load event window.addEventListener( "resize", function () { /* https://stackoverflow.com/questions/8898412/iphone-ipad-triggering-unexpected-resize-events */ // Check window width has actually changed and it's not just a buggy iOS triggering a resize event on scroll if ( window.outerWidth != windowWidth || window.outerHeight != windowHeight ) { /* https://css-tricks.com/snippets/jquery/done-resizing-event/ */ //run code after a time once resizing is done clearTimeout(resizeTimer); resizeTimer = setTimeout(function () { // Run code here, resizing has "stopped" //update the window dimensions for next use windowWidth = window.outerWidth; windowHeight = window.outerHeight; rangeBoxHeight = rangeBox.offsetHeight; if (ddMenu.classList.contains("opened")) { if (rangeBoxHeight > 150) { //MQ(M) or larger body.classList.remove("mobile-menu-opened"); html.classList.remove("mobile-menu-opened"); } else { //MQ(S) or smaller body.classList.add("mobile-menu-opened"); html.classList.add("mobile-menu-opened"); } //end if } //end if checkSubMenuBounds(subMenuUL); }, resizeDelay); } //end if window dimensions check }, true ); //end window resize event window.addEventListener( "scroll", function () { let yEnd; //ending scroll position after timeout has finished let scrollDir; //scrolling direction. >0 = scroll down, <0 = scroll up let header = document.getElementsById("header"); let main = document.getElementsByTagName("main"); let cont = main[0].getElementsByClassName("container")[0]; //first bootstrap container within the main tag that gets paddingTop adjusted on scroll /* https://css-tricks.com/snippets/jquery/done-resizing-event/ */ //run code after a time once resizing is done clearTimeout(scrollTimer); scrollTimer = setTimeout(function () { //Run code here, scrolling has "stopped" rangeBoxHeight = rangeBox.offsetHeight; if (rangeBoxHeight > 150) { //MQ(M) or larger yEnd = window.pageYOffset; scrollDir = yEnd - yStart; yStart = yEnd; if (yEnd > 0) { header[0].classList.add("sticky"); resetDdmenu(ddMenu); if (scrollDir < 0) { //scroll direction is up cont.style.paddingTop = header[0].offsetHeight + "px"; //give padding equal to height of header header[0].classList.add("in"); header[0].classList.remove("out"); } else { //scroll direction is down header[0].classList.add("out"); } } else { resetStickyMenu(header[0], cont); } //end if } else { //MQ(S) or smaller, don't use sticky menu as it takes up too much of the screen resetStickyMenu(header[0], cont); } //end if }, scrollDelay); }, true ); //end window scroll event //begin window orientation change event window.matchMedia("(orientation: portrait)").addListener(function (m) { if (m.matches) { //portrait window.dispatchEvent(new Event("resize")); } else { //landscape window.dispatchEvent(new Event("resize")); } }); // end window orientation change event //add class that overrides default, CSS based hover behavior ddMenu.classList.add("js-enabled"); //submenu ULs let subMenuUL = ddMenu.querySelectorAll("ul a + ul"); //handle clicks outside of menu document.body.addEventListener("click", function (e) { if ( !e.target.closest("#" + ddMenu.getAttribute("id")) || e.target.tagName.toLowerCase() != "a" ) { //if out of menu, or if within menu but not on an anchor //this last check is because the entire menu can span from an apparent logo as the first menu item, for menus having the class of "with-logo", to the last apparent menu item resetDdmenu(ddMenu); } }); //end handle clicks outside of menu //https://stackoverflow.com/questions/3369593/how-to-detect-escape-key-press-with-pure-js-or-jquery //handle escape key being pressed document.body.addEventListener("keydown", function (e) { e = e || window.e; if (e.key === "Escape") { resetDdmenu(ddMenu); } }); //end handle escape key being pressed //begin menu items with internal links //get all links with hashtag let hashLinks = ddMenu.querySelectorAll('a[href^="#"]'); //href starting with # for (let hashLink of hashLinks) { if (hashLink.getAttribute("href").toString().length > 1) { //hashlink is internal link //allow internal links to respond to spacebar keysTriggerClick(hashLink); hashLink.addEventListener("click", function (e) { //reset menu resetDdmenu(ddMenu); //provide proper hash for URL let hashTarget = document.getElementById( hashLink.getAttribute("href").substring(1) ); window.location.hash = hashTarget; hamburgerClose.click(); window.setTimeout(function () { hashTarget.focus(); }, 60); //set timeout to be larger than timeout for hamburger focus timeout }); } //end if } //end menu items with internal links //provide initial aria for subMenuUL subMenuUL.forEach(function (item) { item.setAttribute("aria-hidden", "true"); item.setAttribute("aria-label", "submenu"); }); for (let subMenu of subMenuUL) { //get the anchor followed by a sub menu let subLink = subMenu.parentNode.querySelector("a:first-of-type"); subLink.classList.add("hasSub"); //setup initial aria for each subLink subLink.setAttribute("role", "button"); subLink.setAttribute("aria-haspopup", "true"); subLink.setAttribute("aria-pressed", "false"); subLink.setAttribute("aria-expanded", "false"); //setup keys to trigger a click keysTriggerClick(subLink); subLink.addEventListener( "click", function (e) { e.preventDefault(); //handle open / closed glyphs for subLink, and aria if (this.classList.contains("opened")) { resetDdmenuSublink(this); } else { //if closed resetDdmenuSublink(this); this.classList.add("opened"); this.setAttribute("aria-pressed", "true"); this.setAttribute("aria-expanded", "true"); } //end if //begin check to see if submenu UL is of class selected when clicked if ( subLink.parentNode .querySelector("ul:first-of-type") .classList.contains("selected") ) { //if selected, the submenu is opened, so close it resetDdmenuSubMenu(subLink); subLink.parentNode .querySelector("ul:first-of-type") .classList.remove("selected"); subLink.parentNode .querySelector("ul:first-of-type") .setAttribute("aria-hidden", "true"); //hide from screen readers } else { //if NOT selected, the submenu is closed, so open it resetDdmenuSubMenu(subLink); subLink.parentNode .querySelector("ul:first-of-type") .classList.add("selected"); subLink.parentNode .querySelector("ul:first-of-type") .setAttribute("aria-hidden", "false"); //show to screen readers } //end if }, true ); //subLink.addEventListener('click') } //for(let subMenu of subMenuUL) checkSubMenuBounds(subMenuUL); return; } //end fxn initDdmenu function resetStickyMenu(header, cont) { //removes sticky menu related classes from header and resets the padding of the first bootstrap element in main tag header.classList.remove("sticky"); header.classList.remove("in"); header.classList.remove("out"); cont.style.paddingTop = "0px"; //reset return; } //end fxn resetStickyMenu function checkSubMenuBounds(subMenuUL) { //adjust class of subMenuUL if it extends beyond the right edge of browser window for (let subMenu of subMenuUL) { if (subMenu.classList.contains("shiftLeft")) { subMenu.classList.remove("shiftLeft"); //remove class that shifts subMenu to left side } let depth = 1; let currentMenu = subMenu; while ( currentMenu.parentNode .querySelector("a:first-of-type") .classList.contains("hasSub") ) { depth++; currentMenu = currentMenu.parentNode.parentNode; //walk up the subMenu tree } if (depth > 2) { //subMenu in question normally appears on right side //check its bounds let x = getOffset(subMenu).left; //left x coordinate of subMenu let w = subMenu.offsetWidth; let rc = x + w; //right x coordinate of subMenu let windowWidth = window.innerWidth; if (rc >= windowWidth - w / 2) { // the w/2 is arbitrary but serves to ensure that the submenus shift to the left properly for all MQ > MQ(s) //add class to shift subMenu to left side subMenu.classList.add("shiftLeft"); subMenu.parentNode.parentNode.classList.add("shiftLeft"); } } //end if } //for(let subMenu of subMenuUL) return; } //end fxn checkSubMenuBounds function resetDdmenuSublink(subLink) { //resets current sublink aria and class states subLink.parentNode.parentNode.querySelectorAll("a").forEach(function (item) { item.classList.remove("opened"); item.setAttribute("aria-pressed", "false"); item.setAttribute("aria-expanded", "false"); }); return; } //end fxn resetDdmenuSublink function resetDdmenuSubMenu(subLink) { //resets current submenu tree aria and class states subLink.parentNode.parentNode .querySelectorAll("li > a + ul:first-of-type") .forEach(function (item) { item.classList.remove("selected"); item.setAttribute("aria-hidden", "true"); //hide from screen readers }); return; } //end fxn resetDdmenuSubMenu function resetDdmenu(menu) { //resets aria attributes and classes for menu and link states menu.querySelectorAll('[aria-hidden="false"]').forEach(function (item) { item.setAttribute("aria-hidden", "true"); }); menu.querySelectorAll('[aria-expanded="true"]').forEach(function (item) { item.setAttribute("aria-expanded", "false"); }); menu.querySelectorAll('[aria-pressed="true"]').forEach(function (item) { item.setAttribute("aria-pressed", "false"); }); menu.querySelectorAll(".selected").forEach(function (item) { item.classList.remove("selected"); }); menu.querySelectorAll(".opened").forEach(function (item) { item.classList.remove("opened"); }); //https://stackoverflow.com/questions/2520650/how-do-you-clear-the-focus-in-javascript //remove focus from menu document.activeElement.blur(); return; } //end fxn resetDdmenu function keysTriggerClick(el, keys = [], stopEvent = true, stopProp = true) { //allow for proper aria keydown events, by default, or other events to be triggered on keydown //https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/button_role //el is the element triggering the click event. Must have an ID attribute set for itself in the DOM. //stopEvent & stopProp are booleans to stop event default or propagation //keys are additional keys that trigger the events and must be passed as an array: ['1', '2', 'x', 'y'] //default keys to check: keys.push(" ", "Enter", "Spacebar"); //"Spacebar" for IE11 support el.addEventListener("keydown", function (e) { if (keys.includes(e.key)) { if (stopEvent) { e.preventDefault(); } if (stopProp) { e.stopPropagation(); } el.click(); } //end keydown event for el return; }); //end keydown event } //end fxn keysTriggerClick })(); //end Self-Executing Anonymous Function
That’s all! hopefully, you have successfully created a responsive drop down menu with submenu in Vanilla JavaScript. If you have any questions or suggestions, feel free to comment below.