Navigation Menus in HTML
Build navigation menus in HTML using the semantic nav tag, breadcrumb navigation, mobile hamburger menus, and ARIA attributes for accessibility.
Navigation is the part of your website that people use without thinking about it until it breaks. A visitor lands on your page, looks for where to go next, and if your nav is confusing or inaccessible, they leave. Simple as that.
But here’s the thing: most beginners build navigation menus using just a bunch of <div> tags and links, which technically works, the links show up, you can click them. But there’s a much better way. One that helps search engines understand your site structure, makes your page work for people using screen readers, and follows the kind of standards that actually hold up over time.
In this tutorial, we’re going to cover everything about navigation menus in HTML, from the <nav> element to breadcrumbs, nested dropdowns, mobile menus, and ARIA attributes. Let’s get into it.
01Why Navigation Structure Actually Matters
Before we write a single line of code, let’s talk about why this matters.
We already covered semantic HTML in a previous article. The core idea is that using the right HTML element for the right purpose gives your content meaning not just for browsers, but for assistive technologies, search engines, and developers reading your code months later.
Navigation is one of the clearest examples of this. When you wrap your links inside a <nav> tag instead of a <div>, you’re telling the browser: “this is a navigation region.” Screen readers can jump directly to it. Google can identify it as your site’s primary navigation. And any developer who opens your code file immediately understands what that block is for.
That’s the value of doing navigation right.
02The HTML nav Tag: Your Navigation’s Home Base
The <nav> element was introduced in HTML5 specifically for navigation links. It’s a semantic HTML5 element that groups a set of navigation links together.
The basic syntax looks like this:
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
Notice we’re using a <ul> (unordered list) inside the nav. This is the recommended pattern. Navigation links are essentially a list of options, so using a list element makes semantic sense, and it’s what accessibility guidelines recommend.
You could technically put links directly inside <nav> without a list, and the browser won’t complain. But screen readers will announce a <ul> as “list of X items,” which helps users understand how many options they’re navigating between. Worth doing.
When Should You Use the nav Tag in HTML?
Not every group of links needs a <nav> wrapper. The nav tag in HTML is meant for major navigation blocks, the kind that help users move around your site.
Good candidates for <nav>:
- Your main site header menu
- A sidebar with section links
- A table of contents within an article
- A footer navigation with main page links
- Breadcrumb navigation (more on that soon)
You can have multiple <nav> elements on one page, that’s completely fine. A page might have one for the main header menu and another for a sidebar or footer. We’ll look at how to label them separately using ARIA a bit later.
When You Don’t Need the nav Tag
Skip <nav> for things like:
- Pagination links inside a blog post list (use it if it’s a primary nav, skip it if it’s minor)
- Social media icon links in the footer
- A single “Back to top” link
- Links inside body text (those belong in paragraphs)
The rule of thumb is: if removing this navigation would seriously impact how users find their way around your site, wrap it in <nav>. If it’s a supplemental group of links, a plain <div> or <footer> is fine.
03Building Your First Navigation Menu
Let’s build something real. Here’s a complete, clean navigation menu with proper structure:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Site</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: sans-serif; }
header {
background: #1e293b;
padding: 0 2rem;
}
nav ul {
list-style: none;
display: flex;
gap: 0;
}
nav ul li a {
display: block;
padding: 1.2rem 1rem;
color: #cbd5e1;
text-decoration: none;
font-size: 0.95rem;
transition: color 0.2s;
}
nav ul li a:hover,
nav ul li a[aria-current="page"] {
color: #6366f1;
}
nav ul li a[aria-current="page"] {
font-weight: 600;
}
</style>
</head>
<body>
<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="/" aria-current="page">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
</body>
</html>
There are a few intentional choices in there worth pointing out:
aria-label="Main navigation": labels this nav region for screen readersaria-current="page": tells assistive tech which link is the current page- The active link is visually styled differently so sighted users can see it too
We covered how anchor tags work in depth in our HTML anchor tags tutorial, and how the target and rel attributes work in our link attributes guide. so if any of those look unfamiliar, those are great reads to pair with this one.
Nested Navigation: Adding Dropdowns
Real websites almost always have dropdowns, a top-level link that expands to reveal sub-links. Let’s build that properly.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nested Nav</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: sans-serif; background: #141d34; }
header { background: #1e293b; padding: 0 2rem; }
nav > ul {
list-style: none;
display: flex;
}
nav ul li {
position: relative;
}
nav ul li a {
display: block;
padding: 1.2rem 1rem;
color: #cbd5e1;
text-decoration: none;
font-size: 0.95rem;
white-space: nowrap;
}
nav ul li a:hover { color: #6366f1; }
/* Dropdown submenu */
nav ul li ul {
display: none;
position: absolute;
top: 100%;
left: 0;
background: #1a202c;
border: 1px solid rgba(255,255,255,0.07);
border-radius: 8px;
min-width: 180px;
padding: 0.4rem 0;
z-index: 100;
list-style: none;
}
nav ul li ul li a {
padding: 0.6rem 1rem;
font-size: 0.9rem;
}
/* Show dropdown on hover */
nav ul li:hover > ul,
nav ul li:focus-within > ul {
display: block;
}
/* Dropdown arrow indicator */
nav ul li.has-dropdown > a::after {
content: " ▾";
font-size: 0.75rem;
opacity: 0.6;
}
</style>
</head>
<body>
<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<!-- Dropdown menu -->
<li class="has-dropdown">
<a href="/services" aria-haspopup="true" aria-expanded="false">Services</a>
<ul>
<li><a href="/services/web-design">Web Design</a></li>
<li><a href="/services/development">Development</a></li>
<li><a href="/services/seo">SEO</a></li>
</ul>
</li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
</body>
</html>
A few things to notice in that dropdown:
aria-haspopup="true"tells screen readers “this link opens a submenu”aria-expanded="false"tells them the submenu is currently closed- We use
:focus-withinso keyboard users can also open the dropdown by tabbing into it, not just mouse users - The nested
<ul>inside the<li>is the correct HTML structure for nested navigation
The aria-expanded should ideally be toggled with JavaScript when the dropdown actually opens. The HTML-only version above with :hover and :focus-within is a good starting point, but a real production dropdown would update aria-expanded dynamically.
04Breadcrumb Navigation in HTML
Breadcrumbs are those little “Home › Blog › This Article” trails you see at the top of pages. They tell users where they are in the site hierarchy and let them jump back up any level with one click.
They’re especially useful on sites with deep structures like e-commerce, documentation sites, blogs with categories. If your site only has two levels (home + page), you probably don’t need them. But once you get three or more levels deep, breadcrumbs become genuinely helpful.
Here’s the proper HTML structure for breadcrumb navigation in HTML:
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/blog/html">HTML</a></li>
<li aria-current="page">Navigation Menus</li>
</ol>
</nav>
A few important details here:
- Use an ordered list (
<ol>): breadcrumbs have a meaningful order, so<ol>is more appropriate than<ul> - The last item (current page) gets
aria-current="page"and doesn’t need to be a link, you’re already on that page - Wrap the whole thing in a
<nav>with a uniquearia-labelso screen readers distinguish it from your main navigation
Styling Breadcrumbs with Separators
The visual separators (like › or /) between breadcrumb items should be added with CSS, not HTML. This way, screen readers skip them completely as they’re decorative, not meaningful.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Breadcrumb Example</title>
<style>
.breadcrumb {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
font-size: 0.875rem;
}
.breadcrumb li {
display: flex;
align-items: center;
color: #64748b;
}
/* CSS separator: invisible to screen readers */
.breadcrumb li + li::before {
content: "›";
margin-right: 0.25rem;
color: #475569;
}
.breadcrumb li a {
color: #6366f1;
text-decoration: none;
}
.breadcrumb li a:hover {
text-decoration: underline;
}
.breadcrumb li[aria-current="page"] {
color: #94a3b8;
font-weight: 500;
}
</style>
</head>
<body style="background:#141d34; padding: 2rem;">
<nav aria-label="Breadcrumb">
<ol class="breadcrumb">
<li><a href="/">Home</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/blog/html">HTML</a></li>
<li aria-current="page">Navigation Menus</li>
</ol>
</nav>
</body>
</html>
The key trick is li + li::before. That CSS selector targets every <li> that comes after another <li>, and inserts a separator before it. Clean, purely presentational.
Adding Schema Markup to Breadcrumbs for SEO
Google can display breadcrumbs directly in search results, those little “Home › Blog › Article” snippets you sometimes see under a result. To enable that, you add structured data.
The cleanest way to do this in HTML is with itemscope and itemprop attributes from Schema.org:
<nav aria-label="Breadcrumb">
<ol class="breadcrumb" itemscope itemtype="https://schema.org/BreadcrumbList">
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<a itemprop="item" href="/">
<span itemprop="name">Home</span>
</a>
<meta itemprop="position" content="1">
</li>
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<a itemprop="item" href="/blog">
<span itemprop="name">Blog</span>
</a>
<meta itemprop="position" content="2">
</li>
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
<span itemprop="name" aria-current="page">Navigation Menus</span>
<meta itemprop="position" content="3">
</li>
</ol>
</nav>
Yes, it’s a bit verbose. But for content-heavy sites where breadcrumbs show up in search results, it’s worth it. The position meta tells Google the order of items, and the schema type BreadcrumbList is what triggers the rich result.
🍞 Anatomy of a Breadcrumb Trail
›
›
›
Current page
aria-current=”page”
::before pseudo-element — so screen readers never read it out. Separators are purely visual. The <ol> tag tells screen readers these items have a meaningful sequence.
05ARIA Labels for Navigation
ARIA stands for Accessible Rich Internet Applications. It’s a set of HTML attributes you can add to elements to give assistive technologies more context about what something is and how it behaves.
For navigation, there are a few ARIA attributes you’ll use regularly.
aria-label: Naming Your Nav Regions
When you have multiple <nav> elements on a page, screen readers announce each one as “navigation” which isn’t helpful if there are three of them. aria-label gives each one a unique name.
<!-- Main site navigation -->
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
<!-- Table of contents inside an article -->
<nav aria-label="Table of contents">
<ul>
<li><a href="#intro">Introduction</a></li>
<li><a href="#nav-tag">The nav Tag</a></li>
</ul>
</nav>
<!-- Footer navigation -->
<nav aria-label="Footer navigation">
<ul>
<li><a href="/privacy">Privacy Policy</a></li>
<li><a href="/terms">Terms of Use</a></li>
</ul>
</nav>
A screen reader will now announce these as “Main navigation,” “Table of contents,” and “Footer navigation”, completely distinct regions that users can jump between directly.
aria-labelledby: When the Label is Already on the Page
Sometimes you have a visible heading near your navigation. Instead of repeating the text in an aria-label, you can just point to the heading’s id:
<aside>
<h2 id="sidebar-title">Topics</h2>
<nav aria-labelledby="sidebar-title">
<ul>
<li><a href="/html">HTML</a></li>
<li><a href="/css">CSS</a></li>
<li><a href="/javascript">JavaScript</a></li>
</ul>
</nav>
</aside>
The nav region will now be announced as “Topics navigation” by screen readers, pulling the label directly from the heading. This avoids repeating content and keeps things in sync if you change the heading text, the ARIA label updates automatically.
aria-current for Active Links
We briefly saw this earlier. aria-current="page" marks the link that points to the current page. This is how a screen reader user knows where they are.
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about" aria-current="page">About</a></li> <!-- currently on About -->
<li><a href="/blog">Blog</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
For multi-page websites, you’ll typically set this dynamically via server-side code or JavaScript based on the current URL. But in static HTML examples, you just add it to the matching link manually.
There are other values too: aria-current="step" for wizard/multi-step flows, aria-current="location" for breadcrumbs. For most navigation, "page" is what you want.
aria-expanded for Dropdowns
When you have a dropdown, you need to communicate its open/closed state. This is where aria-expanded comes in:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ARIA Dropdown</title>
<style>
body { font-family: sans-serif; background: #141d34; color: #cbd5e1; padding: 2rem; }
nav ul { list-style: none; padding: 0; display: flex; gap: 0.5rem; }
.has-dropdown { position: relative; }
.has-dropdown > button {
background: #1e293b;
color: #cbd5e1;
border: 1px solid rgba(255,255,255,0.1);
padding: 0.6rem 1rem;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 0.4rem;
}
.has-dropdown > button:hover { background: #1a202c; color: #6366f1; }
.dropdown-menu {
display: none;
position: absolute;
top: calc(100% + 6px);
left: 0;
background: #1a202c;
border: 1px solid rgba(255,255,255,0.08);
border-radius: 8px;
min-width: 160px;
padding: 0.4rem 0;
list-style: none;
z-index: 100;
}
.dropdown-menu.open { display: block; }
.dropdown-menu li a {
display: block;
padding: 0.5rem 1rem;
color: #94a3b8;
text-decoration: none;
font-size: 0.875rem;
}
.dropdown-menu li a:hover { color: #6366f1; background: rgba(99,102,241,0.08); }
.chevron { transition: transform 0.2s; display: inline-block; }
.chevron.up { transform: rotate(180deg); }
</style>
</head>
<body>
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li class="has-dropdown">
<!-- Button is better than <a> here since it doesn't navigate -->
<button
aria-haspopup="true"
aria-expanded="false"
id="services-btn"
aria-controls="services-menu"
onclick="toggleDropdown(this)"
>
Services <span class="chevron">▾</span>
</button>
<ul class="dropdown-menu" id="services-menu" role="menu" aria-labelledby="services-btn">
<li role="none"><a href="/services/design" role="menuitem">Web Design</a></li>
<li role="none"><a href="/services/dev" role="menuitem">Development</a></li>
<li role="none"><a href="/services/seo" role="menuitem">SEO</a></li>
</ul>
</li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
<script>
function toggleDropdown(btn) {
const isOpen = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', !isOpen);
const menu = document.getElementById(btn.getAttribute('aria-controls'));
menu.classList.toggle('open', !isOpen);
btn.querySelector('.chevron').classList.toggle('up', !isOpen);
}
// Close on outside click
document.addEventListener('click', function(e) {
if (!e.target.closest('.has-dropdown')) {
document.querySelectorAll('[aria-expanded="true"]').forEach(btn => {
btn.setAttribute('aria-expanded', 'false');
const menu = document.getElementById(btn.getAttribute('aria-controls'));
if (menu) {
menu.classList.remove('open');
btn.querySelector('.chevron')?.classList.remove('up');
}
});
}
});
</script>
</body>
</html>
Notice that the dropdown trigger is a <button>, not an <a>. That’s intentional. Since clicking “Services” doesn’t navigate anywhere on its own, it just opens a menu, a <button> is semantically correct. Screen readers announce it as a button, not a link, which sets the right expectation.
06Building a Mobile Nav Menu in HTML
Here’s the honest reality: most people visiting your site are on a phone. Your desktop navigation, even a nicely styled horizontal menu won’t fit on a 375px screen. You need a mobile nav menu that collapses and expands on demand.
The classic pattern is the hamburger menu: a button with three horizontal lines that toggles the navigation open and closed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mobile Nav</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: sans-serif; background: #141d34; }
header {
background: #1e293b;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1.5rem;
height: 64px;
border-bottom: 1px solid rgba(255,255,255,0.07);
}
.logo {
color: #ffffff;
font-weight: 700;
font-size: 1.1rem;
text-decoration: none;
}
/* Hamburger button */
.menu-toggle {
background: none;
border: none;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 5px;
padding: 8px;
border-radius: 6px;
}
.menu-toggle span {
display: block;
width: 22px;
height: 2px;
background: #cbd5e1;
border-radius: 2px;
transition: all 0.25s ease;
}
/* Animate to X when open */
.menu-toggle[aria-expanded="true"] span:nth-child(1) {
transform: translateY(7px) rotate(45deg);
}
.menu-toggle[aria-expanded="true"] span:nth-child(2) {
opacity: 0;
}
.menu-toggle[aria-expanded="true"] span:nth-child(3) {
transform: translateY(-7px) rotate(-45deg);
}
/* Navigation */
.main-nav ul {
list-style: none;
display: flex;
gap: 0.25rem;
}
.main-nav ul li a {
display: block;
padding: 0.6rem 0.9rem;
color: #cbd5e1;
text-decoration: none;
font-size: 0.9rem;
border-radius: 6px;
transition: background 0.18s, color 0.18s;
}
.main-nav ul li a:hover { background: rgba(99,102,241,0.15); color: #6366f1; }
/* Mobile styles */
@media (max-width: 640px) {
.menu-toggle { display: flex; }
.main-nav {
position: absolute;
top: 64px;
left: 0;
right: 0;
background: #1e293b;
border-bottom: 1px solid rgba(255,255,255,0.07);
padding: 0.75rem 1.5rem 1rem;
display: none;
z-index: 50;
}
.main-nav.is-open { display: block; }
.main-nav ul {
flex-direction: column;
gap: 0.25rem;
}
}
@media (min-width: 641px) {
.menu-toggle { display: none; }
header { position: static; }
}
header { position: relative; }
</style>
</head>
<body>
<header>
<a class="logo" href="/">MyBrand</a>
<!-- Hamburger button -->
<button
class="menu-toggle"
aria-controls="main-nav"
aria-expanded="false"
aria-label="Toggle navigation menu"
onclick="toggleNav(this)"
>
<span></span>
<span></span>
<span></span>
</button>
<nav class="main-nav" id="main-nav" aria-label="Main navigation">
<ul>
<li><a href="/" aria-current="page">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
<script>
function toggleNav(btn) {
const isOpen = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!isOpen));
document.getElementById('main-nav').classList.toggle('is-open', !isOpen);
}
</script>
</body>
</html>
This example has some good practices built in:
- The hamburger button has
aria-label="Toggle navigation menu", without this, a screen reader would just announce “button” with no context aria-controls="main-nav"links the button to the element it controls, by IDaria-expandedis toggled via JavaScript so screen readers know the current state- The hamburger icon animates to an “X” when open — a visual cue that this is now a close button
- On desktop (641px+), the hamburger hides and the nav shows horizontally. On mobile, the nav hides until toggled
A small but important touch: the three <span> elements making up the hamburger icon are inside a <button>, not a <div>. This means it’s keyboard-focusable and interactive by default, no extra JavaScript needed for that.
07Skip Navigation Links: The Accessibility Feature Most Sites Skip
Here’s one you’ve probably never noticed but have almost certainly encountered: skip navigation links.
If you press Tab on a page with a big navigation menu, a keyboard user has to tab through every single link before they reach the main content. For a menu with 10 items, that’s 10 Tab presses before you can start reading. For someone navigating by keyboard full-time, that’s exhausting.
The solution is a “skip to main content” link placed at the very top of the page, before the navigation. It’s typically hidden visually but becomes visible on focus:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Skip Nav Example</title>
<style>
.skip-link {
position: absolute;
top: -100%;
left: 1rem;
background: #6366f1;
color: #ffffff;
padding: 0.6rem 1.2rem;
border-radius: 0 0 8px 8px;
font-weight: 600;
text-decoration: none;
font-size: 0.9rem;
z-index: 9999;
transition: top 0.2s;
}
.skip-link:focus {
top: 0;
}
</style>
</head>
<body>
<!-- This must be the FIRST focusable element on the page -->
<a class="skip-link" href="#main-content">Skip to main content</a>
<header>
<nav aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/services">Services</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
<main id="main-content">
<h1>Welcome to My Site</h1>
<p>This is the main content area.</p>
</main>
</body>
</html>
We covered this in detail in our fragment identifiers and jump links tutorial, where we walked through building full table of contents navigation with smooth scrolling. Skip links use the same fragment identifier principle applied to accessibility.
The link lives at position top: -100% normally, which pushes it off-screen. The moment it receives keyboard focus, it jumps to top: 0 and becomes visible. One Tab press, one Enter, and the user is at the main content. Clean.
08Navigation Best Practices: What Actually Matters
Let’s bring it all together with a practical list of things that make the difference between good navigation and great navigation.
Keep Navigation Consistent Across Pages
Your navigation should be in the same place on every page. Users build muscle memory and they know your logo is top-left and the nav is top-right. Changing this on different pages breaks that mental model and causes confusion.
Use Descriptive Link Text
Link text like “Click here” or “Read more” is useless on its own. Screen readers give users a list of all links on a page, and “Read more, Read more, Read more” tells them absolutely nothing.
<!-- Bad -->
<a href="/pricing">Click here</a>
<!-- Good -->
<a href="/pricing">View pricing plans</a>
<!-- Also fine if context is clear from surrounding content -->
<p>Check out our plans. <a href="/pricing">See pricing</a></p>
Mark the Active Page
Always add aria-current="page" to the current page’s link, and pair it with a visible active style. Both the Visual and assistive.
Don’t Rely on Color Alone
If your active link is only distinguished by color change, colorblind users may not see it. Add something else like underline, font-weight change, a small indicator dot, a background highlight. Color can be part of the treatment, but shouldn’t be the only signal.
Make Sure Focus Styles are Visible
Keyboard users navigate with Tab, and they need to see where focus is at all times. Don’t do this:
<style>
/* Please don't do this */
a:focus { outline: none; }
/* Do this instead */
a:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 3px;
border-radius: 3px;
}
</style>
Using :focus-visible instead of :focus is the modern approach as it shows the outline for keyboard navigation but suppresses it for mouse clicks, so your design stays clean without sacrificing keyboard accessibility.
Limit Navigation Depth
Three levels deep is usually the maximum comfortable navigation depth: main nav → dropdown → sub-dropdown. Beyond that, most users get lost. If your site has that much content, consider using a sidebar or dedicated category pages instead of deeply nested dropdowns.
Test Your Navigation Without CSS
Try disabling CSS in your browser and see if your navigation still makes sense. If the structure is good HTML, it will. If it completely falls apart, your markup might be relying too heavily on visual presentation rather than semantic structure.
09Putting It All Together: A Complete Navigation Example
Here’s everything we’ve covered in one complete, production-ready navigation component. This includes a header with a main nav, a breadcrumb trail below it, proper ARIA attributes, mobile responsiveness, and a skip link:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Complete Navigation Example</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, sans-serif;
background: #141d34;
color: #cbd5e1;
}
/* Skip link */
.skip-link {
position: absolute;
top: -100%;
left: 1rem;
background: #6366f1;
color: #fff;
padding: 0.5rem 1rem;
border-radius: 0 0 8px 8px;
font-weight: 600;
text-decoration: none;
font-size: 0.875rem;
z-index: 9999;
}
.skip-link:focus { top: 0; }
/* Header */
header {
background: #1e293b;
border-bottom: 1px solid rgba(255,255,255,0.06);
position: relative;
}
.header-inner {
max-width: 1100px;
margin: 0 auto;
padding: 0 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
height: 64px;
}
.logo {
color: #fff;
font-weight: 700;
font-size: 1.1rem;
text-decoration: none;
letter-spacing: -0.02em;
}
.logo span { color: #6366f1; }
/* Main nav */
.main-nav ul {
list-style: none;
display: flex;
gap: 0.1rem;
}
.main-nav ul li a {
display: block;
padding: 0.5rem 0.85rem;
color: #94a3b8;
text-decoration: none;
font-size: 0.9rem;
border-radius: 6px;
transition: background 0.15s, color 0.15s;
}
.main-nav ul li a:hover {
background: rgba(99,102,241,0.12);
color: #6366f1;
}
.main-nav ul li a[aria-current="page"] {
color: #6366f1;
background: rgba(99,102,241,0.12);
font-weight: 500;
}
.main-nav ul li a:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 2px;
}
/* Hamburger */
.menu-toggle {
display: none;
background: none;
border: 1px solid rgba(255,255,255,0.12);
cursor: pointer;
padding: 7px;
border-radius: 6px;
flex-direction: column;
gap: 4px;
}
.menu-toggle span {
display: block;
width: 20px;
height: 2px;
background: #cbd5e1;
border-radius: 2px;
transition: all 0.22s ease;
}
.menu-toggle[aria-expanded="true"] span:nth-child(1) {
transform: translateY(6px) rotate(45deg);
}
.menu-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.menu-toggle[aria-expanded="true"] span:nth-child(3) {
transform: translateY(-6px) rotate(-45deg);
}
/* Breadcrumb */
.breadcrumb-bar {
background: #0f1729;
border-bottom: 1px solid rgba(255,255,255,0.04);
padding: 0.6rem 1.5rem;
}
.breadcrumb-bar .inner {
max-width: 1100px;
margin: 0 auto;
}
.breadcrumb {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 0.2rem;
font-size: 0.8rem;
}
.breadcrumb li {
display: flex;
align-items: center;
color: #475569;
}
.breadcrumb li + li::before {
content: "›";
margin-right: 0.2rem;
color: #334155;
}
.breadcrumb li a {
color: #6366f1;
text-decoration: none;
}
.breadcrumb li a:hover { text-decoration: underline; }
.breadcrumb li[aria-current="page"] { color: #64748b; }
/* Main content */
main {
max-width: 1100px;
margin: 0 auto;
padding: 3rem 1.5rem;
}
main h1 {
font-size: 1.8rem;
color: #fff;
margin-bottom: 1rem;
}
main p { color: #94a3b8; line-height: 1.7; }
/* Mobile */
@media (max-width: 640px) {
.menu-toggle { display: flex; }
.main-nav {
display: none;
position: absolute;
top: 64px;
left: 0;
right: 0;
background: #1e293b;
border-bottom: 1px solid rgba(255,255,255,0.06);
padding: 0.75rem 1.5rem 1rem;
z-index: 40;
}
.main-nav.is-open { display: block; }
.main-nav ul {
flex-direction: column;
gap: 0.2rem;
}
}
</style>
</head>
<body>
<!-- 1. Skip link -->
<a class="skip-link" href="#main-content">Skip to main content</a>
<!-- 2. Header with main nav -->
<header>
<div class="header-inner">
<a class="logo" href="/">Daniyal<span>.dev</span></a>
<button
class="menu-toggle"
aria-controls="main-nav"
aria-expanded="false"
aria-label="Toggle navigation menu"
onclick="toggleNav(this)"
>
<span></span>
<span></span>
<span></span>
</button>
<nav class="main-nav" id="main-nav" aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/blog" aria-current="page">Blog</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</div>
</header>
<!-- 3. Breadcrumb navigation -->
<div class="breadcrumb-bar">
<div class="inner">
<nav aria-label="Breadcrumb">
<ol class="breadcrumb">
<li><a href="/">Home</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/blog/html">HTML</a></li>
<li aria-current="page">Navigation Menus</li>
</ol>
</nav>
</div>
</div>
<!-- 4. Main content -->
<main id="main-content">
<h1>Navigation Menus in HTML</h1>
<p>Your main content lives here. Keyboard users can skip straight to this point using the skip link at the top of the page.</p>
</main>
<script>
function toggleNav(btn) {
const isOpen = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!isOpen));
document.getElementById('main-nav').classList.toggle('is-open', !isOpen);
}
</script>
</body>
</html>
That’s a complete, real-world navigation setup. Resize the browser window to see the hamburger menu kick in on smaller screens. Tab through with your keyboard to see the skip link appear. It covers the full picture.
📋 ARIA Attributes for Navigation: Quick Reference
| Attribute | Values | What it does |
|---|---|---|
| aria-label | “Main navigation” | Names a nav region so screen readers can distinguish between multiple navs on a page |
| aria-labelledby | “heading-id” | Names a nav region by pointing to an existing visible heading’s ID |
| aria-current | “page” / “step” | Marks the link that corresponds to the current page or step |
| aria-expanded | “true” / “false” | Tells screen readers whether a dropdown or mobile menu is currently open |
| aria-haspopup | “true” / “menu” | Indicates that a button or link opens a popup or submenu |
| aria-controls | “element-id” | Links a control (like a button) to the element it controls, by ID |
10A Quick Note on the HTML nav Tag and SEO
Google’s crawlers understand the <nav> element. Using it correctly signals that the links inside are navigation links, not just body content. This helps Google understand your site’s structure, which links are primary, and how pages relate to each other.
Breadcrumb navigation has a more direct SEO benefit: add the Schema.org BreadcrumbList markup we showed earlier, and Google can display those breadcrumb trails in your search result snippets. This can improve click-through rates, since users see at a glance that your content is organized and relevant.
More broadly, when Google’s crawler visits your site, it uses the navigation structure to understand what your most important pages are. Well-structured navigation with clear, descriptive link text passes that context along. Our earlier articles on semantic HTML and the HTML document structure go deeper on how structure and SEO connect. Worth reading if you haven’t already.
11Common Navigation Mistakes Worth Avoiding
To round things out, here are the mistakes that come up again and again:
Using divs instead of nav. We’ve covered this. Use <nav>. It’s there for a reason.
Missing aria-label on multiple navs. If you have more than one <nav>, each one needs a unique label. Otherwise screen reader users hear “navigation, navigation, navigation” with no way to tell them apart.
Not marking the active page. Add aria-current="page" to the current page’s link. Always.
Using a link as a dropdown trigger when it doesn’t navigate. If clicking a “Services” item just opens a dropdown without going anywhere, use a <button>, not an <a href>.
Forgetting about keyboard navigation. Tab through your navigation. Can you reach every link? Does the dropdown open and close with the keyboard? Can you see where focus is? If any of those are “no,” fix it.
Not including a skip link. Every page with a navigation menu should have a skip link as the first focusable element. It takes about 10 minutes to add and makes a real difference for keyboard users.
Over-nesting dropdowns. Two levels is usually enough. Three is the max. If you’re going four or five levels deep, your information architecture needs rethinking more than your HTML does.
12You’re Ready to Build Better Navigation
Navigation menus might seem like a small part of a webpage, but they’re one of the most critical pieces of the whole structure. They’re how people find things, how Google understands your site, and how assistive technology users move through your content.
What you’ve learned here, the HTML nav tag, breadcrumb patterns, ARIA attributes, the mobile hamburger menu, and skip links, is genuinely the foundation that production websites are built on. Not some theoretical best-practice that nobody actually follows. Real stuff, used on real sites.
Keep practicing by building a nav for a project you’re already working on. Start simple, a basic horizontal menu with <nav> and proper aria-label. Then add breadcrumbs if the site structure calls for it. Then layer in the mobile toggle. One piece at a time.
Up next in the series, we’ll be diving into HTML Tables: when to use them, how to structure them accessibly, and the attributes that most people miss. See you there.
Navigation Menus in HTML
Loading challenge…
Ready to Practice?
Select a difficulty level above to load your challenge.