Multi-level Responsive Navigation (Dynamic)
Multi-level Responsive Navigation (Dynamic)
==========================================
This project demonstrates a responsive, accessible, dynamic multi-level navigation built with HTML, CSS and JavaScript.
It programmatically generates 20 top-level menu items with 6 sublinks each (120 sublinks total), includes responsive
hamburger menu,
collapsible submenus, smooth open/close animations, keyboard accessibility, and a dark-mode toggle stored in
localStorage.
How it works:
- HTML contains a minimal scaffold and placeholders.
- CSS provides layout, responsive behaviour, transitions and accessible focus styles.
- JS generates the menu structure (so you get 120+ links without manually typing them) and manages interactions:
- toggling the mobile menu
- expanding/collapsing submenus
- keyboard navigation (Arrow keys, Enter, Esc)
- smooth scroll-to-anchor for demonstration
- dark-mode persistence.
You can copy the full code below into a single `index.html` file and open it in the browser.
Full Code (copy into index.html):
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Multi Navigation — Demo</title>
<style>
:root{
--bg: #ffffff; --fg: #111827; --muted:#6b7280; --accent:#06b6d4;
--nav-bg:#f8fafc; --nav-border: rgba(15,23,42,0.06);
--radius:8px;
}
[data-theme="dark"]{
--bg: #0b1220; --fg: #e6eef8; --muted:#9aa6b2; --accent:#7dd3fc;
--nav-bg:#071022; --nav-border: rgba(255,255,255,0.06);
}
*{box-sizing:border-box}
body{font-family:Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; margin:0;b
.container{max-width:1200px;margin:0 auto;padding:1rem;}
.header{display:flex;align-items:center;justify-content:space-between;gap:1rem;background:var(--nav-bg);padding:.5rem 1r
.brand{font-weight:700;display:flex;gap:.5rem;align-items:center}
.controls{display:flex;gap:.5rem;align-items:center}
button.icon{background:transparent;border:0;padding:.4rem;cursor:pointer;font-size:1rem;border-radius:6px}
button.icon:focus{outline:3px solid rgba(6,182,212,.18)}
/* NAV */
.nav-wrap{display:flex;align-items:center;gap:1rem}
nav.primary{display:flex;gap:0.5rem;align-items:stretch}
nav.primary ul{list-style:none;margin:0;padding:0;display:flex;gap:.25rem}
nav.primary li{position:relative}
nav.primary > ul > li > a{display:inline-flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-radius:6px;text-d
nav.primary a:focus{outline:3px solid rgba(6,182,212,.12)}
/* dropdowns */
.submenu{position:absolute;left:0;top:calc(100% + 6px);min-width:240px;background:var(--bg);border:1px solid var(--nav-b
li.open > .submenu, li:hover > .submenu{opacity:1;transform:translateY(0) scale(1);pointer-events:auto}
/* nested submenu inside submenu (flyout) */
.submenu ul{display:block}
.submenu .has-children{position:relative}
.submenu .has-children > .submenu{left:100%;top:0;margin-left:6px}
/* mobile */
.mobile-toggle{display:none}
@media (max-width: 900px){
nav.primary{display:none;position:relative}
.mobile-toggle{display:block}
.mobile-nav{display:block;width:100%;}
.mobile-panel{position:fixed;inset:0;background:rgba(2,6,23,.5);backdrop-filter:blur(4px);display:none;padding:1rem;z-
.mobile-panel.open{display:block}
.mobile-menu{background:var(--bg);max-width:420px;height:100%;overflow:auto;padding:1rem;border-radius:8px;box-shadow:
.mobile-menu ul{display:block}
.mobile-menu a{display:flex;justify-content:space-between;padding:.6rem .75rem;border-radius:6px;text-decoration:none;
.mobile-menu .submenu{position:static;opacity:1;transform:none;border:0;padding-left:.5rem;display:none}
.mobile-menu .open > .submenu{display:block}
.mobile-menu .chev{transform:rotate(0);transition:transform .15s ease}
.mobile-menu .open > a .chev{transform:rotate(90deg)}
}
/* buttons and utility */
.kv{font-size:.9rem;color:var(--muted)}
.small{font-size:.85rem}
.theme-toggle{padding:.4rem .6rem;border-radius:6px;background:linear-gradient(180deg,rgba(0,0,0,.02),transparent);borde
/* accessibility focus */
a:focus, button:focus{outline-offset:2px}
/* content demo anchors */
section.demo{padding:3rem 1rem;border-bottom:1px dashed var(--nav-border);min-height:36vh}
.demo h2{margin-top:0}
</style>
</head>
<body>
<header class="header">
<div class="brand container">
<div style="display:flex;align-items:center;gap:.5rem">
<strong>MultiNav</strong><span class="kv small">Demo</span>
</div>
<div class="controls">
<button class="icon mobile-toggle" id="mobileBtn" aria-label="Open menu" aria-expanded="false">■</button>
<button class="theme-toggle" id="themeToggle" aria-pressed="false">Toggle Theme</button>
</div>
</div>
</header>
<div class="nav-wrap container" aria-label="Main navigation area">
<nav class="primary" id="primaryNav" aria-label="Primary navigation" role="navigation">
<!-- menu will be generated here -->
</nav>
</div>
<!-- mobile panel -->
<div class="mobile-panel" id="mobilePanel" aria-hidden="true">
<div class="mobile-menu" role="dialog" aria-modal="true" aria-label="Mobile menu" id="mobileMenu">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:.5rem">
<strong>Menu</strong>
<button class="icon" id="mobileClose" aria-label="Close menu">✕</button>
</div>
<nav class="mobile-nav" id="mobileNav" aria-label="Mobile navigation">
<!-- mobile menu generated here -->
</nav>
</div>
</div>
<main class="container">
<section class="demo" id="section-home"><h2>Home</h2><p>Demo content area — click links to test smooth scroll.</p></se
<section class="demo" id="section-1"><h2>Section 1</h2><p>Section 1 content.</p></section>
<section class="demo" id="section-2"><h2>Section 2</h2><p>Section 2 content.</p></section>
<section class="demo" id="section-3"><h2>Section 3</h2><p>Section 3 content.</p></section>
</main>
<script>
/* Configuration: generate 20 top-level items, each with 6 sublinks = 120 links */
const TOP = 20;
const SUB = 6;
/* Utility to create link element */
function makeLink(href, text){ const a = document.createElement('a'); a.href = href; a.textContent = text; a.setAttribut
/* Build menu data */
const menuData = Array.from({length: TOP}, (_,i) => {
const id = i+1;
return {
title: `Category ${id}`,
href: `#section-${(id%3)}`,
children: Array.from({length: SUB}, (_,j) => ({
title: `Item ${id}.${j+1}`,
href: `#section-${(j%3)}`,
children: j % 3 === 0 ? [ // some deeper nesting for variety
{ title: `Deep ${id}.${j+1}.1`, href: `#section-${(j%3)}` },
{ title: `Deep ${id}.${j+1}.2`, href: `#section-${(j%3)}` }
]: []
}))
}
});
/* Create desktop nav markup */
function buildDesktopNav(data){
const nav = document.getElementById('primaryNav');
const ul = document.createElement('ul');
ul.setAttribute('role','menubar');
data.forEach(group => {
const li = document.createElement('li');
li.setAttribute('role','none');
const a = makeLink(group.href, group.title);
a.setAttribute('role','menuitem');
a.setAttribute('aria-haspopup', group.children.length ? 'true' : 'false');
a.setAttribute('tabindex', '0');
li.appendChild(a);
if(group.children && group.children.length){
const sub = document.createElement('div');
sub.className = 'submenu';
const subul = document.createElement('ul');
subul.setAttribute('role','menu');
group.children.forEach(child => {
const subli = document.createElement('li');
subli.setAttribute('role','none');
const ca = makeLink(child.href, child.title);
ca.setAttribute('tabindex', '-1');
subli.appendChild(ca);
if(child.children && child.children.length){
subli.classList.add('has-children');
const fly = document.createElement('div'); fly.className = 'submenu';
const flyul = document.createElement('ul');
child.children.forEach(dc => {
const dli = document.createElement('li');
const da = makeLink(dc.href, dc.title);
da.setAttribute('tabindex','-1');
dli.appendChild(da);
flyul.appendChild(dli);
});
fly.appendChild(flyul);
subli.appendChild(fly);
}
subul.appendChild(subli);
});
sub.appendChild(subul);
li.appendChild(sub);
}
ul.appendChild(li);
});
nav.appendChild(ul);
}
/* Create mobile nav markup (collapsible) */
function buildMobileNav(data){
const mnav = document.getElementById('mobileNav');
const ul = document.createElement('ul');
data.forEach((group,gi) => {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = group.href; a.textContent = group.title;
a.setAttribute('role','menuitem');
const chevron = document.createElement('span'); chevron.className='chev'; chevron.textContent='›';
a.appendChild(chevron);
li.appendChild(a);
if(group.children && group.children.length){
const sub = document.createElement('div'); sub.className='submenu';
const subul = document.createElement('ul');
group.children.forEach(child => {
const subli = document.createElement('li');
const ca = document.createElement('a'); ca.href = child.href; ca.textContent = child.title;
subli.appendChild(ca);
if(child.children && child.children.length){
const flyul = document.createElement('ul');
child.children.forEach(dc => {
const dli = document.createElement('li');
const da = document.createElement('a'); da.href = dc.href; da.textContent = dc.title;
flyul.appendChild(dli); dli.appendChild(da);
});
subli.appendChild(flyul);
}
subul.appendChild(subli);
});
sub.appendChild(subul);
li.appendChild(sub);
// click to toggle submenu on mobile
a.addEventListener('click', (e) => {
e.preventDefault();
li.classList.toggle('open');
});
} else {
// close mobile panel after link selection
a.addEventListener('click', () => closeMobile());
}
ul.appendChild(li);
});
mnav.appendChild(ul);
}
/* toggle mobile */
const mobileBtn = document.getElementById('mobileBtn');
const mobilePanel = document.getElementById('mobilePanel');
const mobileClose = document.getElementById('mobileClose');
function openMobile(){
mobilePanel.classList.add('open');
mobilePanel.setAttribute('aria-hidden','false');
mobileBtn.setAttribute('aria-expanded','true');
}
function closeMobile(){
mobilePanel.classList.remove('open');
mobilePanel.setAttribute('aria-hidden','true');
mobileBtn.setAttribute('aria-expanded','false');
}
mobileBtn.addEventListener('click', openMobile);
mobileClose.addEventListener('click', closeMobile);
mobilePanel.addEventListener('click', (e)=>{ if(e.target===mobilePanel) closeMobile(); });
/* keyboard accessibility for desktop nav */
function addDesktopKeyboard(nav){
nav.querySelectorAll('a[role="menuitem"]').forEach(a => {
a.addEventListener('keydown', (e) => {
const li = a.parentElement;
if(e.key === 'ArrowDown'){
// open submenu if available and focus first item
const submenu = li.querySelector('.submenu');
if(submenu){
li.classList.add('open');
const first = submenu.querySelector('a[role="menuitem"], a');
if(first) first.focus();
}
e.preventDefault();
} else if(e.key === 'Escape'){
li.classList.remove('open');
a.blur();
}
});
});
}
/* smooth scroll for anchors */
document.addEventListener('click', (e) => {
const a = e.target.closest('a');
if(a && a.getAttribute('href') && a.getAttribute('href').startsWith('#')){
const id = a.getAttribute('href').slice(1);
const el = document.getElementById(id);
if(el){ e.preventDefault(); el.scrollIntoView({behavior:'smooth', block:'start'}); closeMobile(); }
}
});
/* dark mode toggle */
const themeToggle = document.getElementById('themeToggle');
function setTheme(t){ document.documentElement.setAttribute('data-theme', t); themeToggle.setAttribute('aria-pressed', t
themeToggle.addEventListener('click', ()=> setTheme(document.documentElement.getAttribute('data-theme') === 'dark' ? 'li
/* restore */
if(localStorage.getItem('site-theme') === 'dark') setTheme('dark');
/* init */
buildDesktopNav(menuData);
buildMobileNav(menuData);
addDesktopKeyboard(document.getElementById('primaryNav'));
/* simple click outside to close desktop submenu */
document.addEventListener('click', (e) => {
const nav = document.getElementById('primaryNav');
if(!nav.contains(e.target)){
nav.querySelectorAll('li.open').forEach(li => li.classList.remove('open'));
}
});
</script>
</body>
</html>
Usage Notes
Usage notes:
- Save the content above to a file named `index.html` and open in a modern browser.
- The menu is generated dynamically: you can change TOP and SUB constants to create more or fewer items.
- Mobile behavior: click the ■ icon to open the mobile menu. Click chevrons to expand submenus on mobile.
- Accessibility: basic keyboard support included; consider enhancing with roving tabindex for full a11y.
- Dark mode persists via localStorage.