The nav tag in HTML: when to use it and how to label it

The nav element marks a block of major navigation links. It changes nothing on screen, but it turns the block into a landmark that assistive technology can list and jump to.

The <nav> tag wraps a block of major navigation links: the site menu, a table of contents, a breadcrumb, a pagination bar.

It looks like a plain block until you style it. What it adds is meaning. The browser exposes it as a navigation landmark, which screen readers can list and jump to.

<nav aria-label="Main">
  <ul>
    <li><a href="/guides" aria-current="page">Guides</a></li>
    <li><a href="/api">API</a></li>
    <li><a href="/pricing">Pricing</a></li>
  </ul>
</nav>

The example below reads the landmarks from a small page with querySelectorAll and lists them. Turn the labels off, or add a nav to the footer, and watch the list change.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>nav landmark viewer</title>
<style>
  body { margin: 0; padding: 12px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .wrap { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
  @media (max-width: 560px) { .wrap { grid-template-columns: 1fr; } }
  #page { background: #fff; border-radius: 10px; padding: 10px; font-size: 13px; }
  #page header, #page main, #page footer, #page nav { border: 1px dashed #c3c8d1; border-radius: 8px; padding: 6px 8px; margin: 6px 0; }
  #page nav { border-color: #2563eb; background: #eff5ff; }
  #page ul { margin: 0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: 10px; }
  #page a { color: #1d4ed8; }
  .panel { background: #fff; border-radius: 10px; padding: 10px 12px; font-size: 13px; }
  .panel h3 { margin: 0 0 8px; font-size: 14px; }
  #list { margin: 0 0 8px; padding-left: 18px; line-height: 1.6; }
  .warn { background: #fff4ec; color: #9a3412; border-radius: 6px; padding: 6px 8px; margin: 6px 0 0; }
  .ok { background: #ecf8f0; color: #0f5132; border-radius: 6px; padding: 6px 8px; margin: 6px 0 0; }
  label { display: block; margin: 4px 0; }
</style>
</head>
<body>
<div class="wrap">
  <div id="page">
    <header>
      <b>Acme Docs</b>
      <nav aria-label="Main">
        <ul><li><a href="#">Guides</a></li><li><a href="#">API</a></li><li><a href="#">Pricing</a></li></ul>
      </nav>
    </header>
    <main>
      <nav aria-label="On this page">
        <ul><li><a href="#">Install</a></li><li><a href="#">Configure</a></li><li><a href="#">Deploy</a></li></ul>
      </nav>
      <p>Article text goes here.</p>
    </main>
    <footer id="foot">
      <ul><li><a href="#">Terms</a></li><li><a href="#">Privacy</a></li><li><a href="#">Contact</a></li></ul>
    </footer>
  </div>

  <div class="panel">
    <h3>Landmarks on this page</h3>
    <ol id="list"></ol>
    <div id="msg"></div>
    <label><input type="checkbox" id="nolabels"> Remove the aria-labels</label>
    <label><input type="checkbox" id="footnav"> Wrap the footer links in a nav too</label>
  </div>
</div>

<script>
  const page = document.getElementById('page');
  // element -> landmark role (top-level header/footer only, as in this sample)
  const roles = { HEADER: 'banner', NAV: 'navigation', MAIN: 'main', ASIDE: 'complementary', FOOTER: 'contentinfo' };

  function render() {
    const items = page.querySelectorAll('header, nav, main, aside, footer');
    const list = document.getElementById('list');
    list.innerHTML = '';
    items.forEach((el) => {
      const name = el.getAttribute('aria-label');
      const li = document.createElement('li');
      li.textContent = roles[el.tagName] + (name ? ' "' + name + '"' : '');
      list.appendChild(li);
    });
    // every nav needs a name once there is more than one
    const navs = page.querySelectorAll('nav');
    const unnamed = [...navs].filter((n) => !n.getAttribute('aria-label')).length;
    const msg = document.getElementById('msg');
    if (navs.length > 1 && unnamed) {
      msg.className = 'warn';
      msg.textContent = navs.length + ' navigation landmarks, ' + unnamed + ' without a label. A landmark list cannot tell them apart.';
    } else {
      msg.className = 'ok';
      msg.textContent = navs.length + ' navigation landmarks, each with its own label.';
    }
    if (document.querySelector('#foot nav')) {
      msg.textContent += ' The footer nav is optional: contentinfo already marks those links.';
    }
  }

  document.getElementById('nolabels').addEventListener('change', (e) => {
    page.querySelectorAll('nav').forEach((n) => {
      if (e.target.checked) { n.dataset.label = n.getAttribute('aria-label') || ''; n.removeAttribute('aria-label'); }
      else if (n.dataset.label) n.setAttribute('aria-label', n.dataset.label);
    });
    render();
  });

  document.getElementById('footnav').addEventListener('change', (e) => {
    const foot = document.getElementById('foot');
    if (e.target.checked) {
      const nav = document.createElement('nav');
      if (!document.getElementById('nolabels').checked) nav.setAttribute('aria-label', 'Footer');
      else nav.dataset.label = 'Footer';
      nav.appendChild(foot.querySelector('ul'));
      foot.appendChild(nav);
    } else {
      const nav = foot.querySelector('nav');
      foot.appendChild(nav.querySelector('ul'));
      nav.remove();
    }
    render();
  });

  render();
</script>
</body>
</html>
A sample page on the left, its landmarks on the right. The toggles remove the labels or add a footer nav.

This article is about the element and its structure. For the flexbox layout, colours and the narrow screen pass, see HTML CSS navbar.

What the nav element does

HTML has a handful of elements that map to landmark roles. A top-level <header> is a banner, <main> is main, a top-level <footer> is contentinfo, and every <nav> is navigation.

Each landmark element on the left becomes an entry that assistive technology can list and jump to.
Each landmark element on the left becomes an entry that assistive technology can list and jump to.

Screen readers offer commands to list landmarks or move to the next one. A user who knows the menu is in a navigation landmark can reach it, or skip it, in one step instead of tabbing through every link.

Nothing changes visually. <nav> is a block element with no default styles beyond that, so it looks exactly like a <div>.

The difference is only in meaning, and meaning is what other software reads.

<div> <nav>
Default look Block, no styles Block, no styles
Role exposed None navigation
Shows up in a landmark list No Yes
Needs extra attributes role="navigation" to match No

You can give a div role="navigation" and get the same landmark. Using <nav> gets there with no attribute, which is why semantic HTML prefers the native element.

When to use nav, and when not to

The HTML standard says <nav> is for major navigation blocks. Not every group of links qualifies.

Blocks that help people move around the site get a nav. Small link groups do not need one.
Blocks that help people move around the site get a nav. Small link groups do not need one.

Good candidates:

  • The main site menu in the header.
  • A table of contents for a long page.
  • A breadcrumb trail.
  • Pagination between pages of results.

Usually not needed:

  • A short list of footer links such as terms, privacy and contact. The standard notes that <footer> alone is enough for these.
  • A row of share or social icons.
  • Links inside a paragraph of text.

Every extra nav adds one more entry to the landmark list. Keep the list short enough to be useful.

Put a list inside

A nav does not have to contain a list. The standard allows any flow content, even a sentence with links. In practice a list is the better default.

With a <ul>, a screen reader announces the list and its item count before reading the links, so the user knows how long the menu is.

Use <ol> for a breadcrumb, where the order is the point. The ul tag covers removing the bullets and laying the items out in a row.

Keep the site logo outside the list. It is a link home, not a menu item.

Label every nav when there is more than one

One nav on a page needs no name. Two or more do, or the landmark list shows the same entry twice and the user has to enter each one to find out which is which.

Without labels, two navs look the same in a landmark list. aria-label gives each a name.
Without labels, two navs look the same in a landmark list. aria-label gives each a name.

Give each nav a short aria-label:

<nav aria-label="Main">...</nav>
<nav aria-label="Breadcrumb">...</nav>
<nav aria-label="Pagination">...</nav>

Do not add the word "navigation" to the label. The role is announced already, so a label of "Main navigation" ends up read as "Main navigation navigation". If a visible heading already names the block, point to it with aria-labelledby instead.

Mark the current page with aria-current

A menu highlights the current page with colour, but colour is not announced. aria-current="page" on the link is. Screen readers announce it along with the link, so the user hears which page they are on.

It also makes a good CSS hook, so the visual and spoken states cannot drift apart:

nav a[aria-current="page"] { font-weight: 700; }

Breadcrumbs and pagination use the same attribute. Try both below with the mouse, or press Tab to move through the links and Enter to follow one.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Breadcrumb and pagination navs</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .crumbs ol { display: flex; flex-wrap: wrap; margin: 0; padding: 0; list-style: none; font-size: 14px; }
  .crumbs li + li::before { content: "/"; margin: 0 8px; color: #9aa3b2; }
  .crumbs [aria-current="page"] { color: #1d2330; font-weight: 600; text-decoration: none; }
  a { color: #1d4ed8; }
  a:focus-visible { outline: 3px solid #f59e0b; outline-offset: 2px; border-radius: 4px; }
  .card { background: #fff; border-radius: 10px; padding: 14px; margin: 14px 0; min-height: 60px; }
  .pages ul { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; list-style: none; }
  .pages a { display: inline-block; min-width: 40px; padding: 9px 10px; text-align: center; border-radius: 8px; background: #fff; text-decoration: none; }
  .pages a[aria-current="page"] { background: #1d4ed8; color: #fff; font-weight: 700; }
  .pages a[aria-disabled="true"] { color: #9aa3b2; }
  #status { font-size: 13px; color: #4b5563; margin-top: 10px; }
  code { background: #e8ebf0; border-radius: 4px; padding: 0 4px; }
</style>
</head>
<body>
<nav class="crumbs" aria-label="Breadcrumb">
  <ol>
    <li><a href="#home">Home</a></li>
    <li><a href="#guides">Guides</a></li>
    <li><a href="#nav" aria-current="page">The nav element</a></li>
  </ol>
</nav>

<div class="card" id="content">Results, page 1</div>

<nav class="pages" aria-label="Pagination">
  <ul>
    <li><a id="prev" aria-disabled="true">Previous</a></li>
    <li><a href="#p1" aria-current="page">1</a></li>
    <li><a href="#p2">2</a></li>
    <li><a href="#p3">3</a></li>
    <li><a href="#p4">4</a></li>
    <li><a id="next" href="#p2">Next</a></li>
  </ul>
</nav>
<div id="status"></div>

<script>
  const nav = document.querySelector('.pages');
  const last = 4;

  function go(n) {
    // move aria-current to the page link that matches
    nav.querySelectorAll('a[href^="#p"]:not(#prev):not(#next)').forEach((a) => {
      if (a.getAttribute('href') === '#p' + n) a.setAttribute('aria-current', 'page');
      else a.removeAttribute('aria-current');
    });
    // Previous / Next: a link with no href is not focusable, which suits a dead end
    setEdge(document.getElementById('prev'), n > 1 ? n - 1 : null);
    setEdge(document.getElementById('next'), n < last ? n + 1 : null);
    document.getElementById('content').textContent = 'Results, page ' + n;
    document.getElementById('status').innerHTML = 'aria-current="page" is on link <code>' + n + '</code>';
  }

  function setEdge(a, target) {
    if (target) { a.setAttribute('href', '#p' + target); a.removeAttribute('aria-disabled'); }
    else { a.removeAttribute('href'); a.setAttribute('aria-disabled', 'true'); }
  }

  nav.addEventListener('click', (e) => {
    const a = e.target.closest('a[href]');
    if (!a) return;
    e.preventDefault(); // a real site would load the next page here
    go(Number(a.getAttribute('href').slice(2)));
  });

  go(1);
</script>
</body>
</html>
A breadcrumb and a pagination bar, each a labelled nav. Clicking a page moves aria-current to it.

The disabled Previous link has no href. A link without href is not focusable, so Tab skips it, and aria-disabled="true" says why it is grey.

The last piece is the skip link: the first link on the page, pointing to <main>. Keyboard users press Tab once, see it, and press Enter to jump past the whole menu.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Header with skip link and main nav</title>
<style>
  body { margin: 0; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  /* skip link: off screen until it receives keyboard focus */
  .skip { position: absolute; left: 8px; top: -60px; padding: 10px 14px; background: #111827; color: #fff; border-radius: 8px; z-index: 10; }
  .skip:focus { top: 8px; }
  .bar { display: flex; align-items: center; gap: 16px; padding: 12px 16px; background: #fff; border-bottom: 1px solid #e1e4ea; }
  .logo { font-weight: 800; color: #1d2330; text-decoration: none; }
  .bar nav { margin-left: auto; }
  #menu { display: flex; gap: 18px; margin: 0; padding: 0; list-style: none; }
  #menu a { color: #374151; text-decoration: none; padding: 6px 0; }
  #menu a[aria-current="page"] { color: #1d4ed8; font-weight: 700; border-bottom: 2px solid #1d4ed8; }
  a:focus-visible, button:focus-visible { outline: 3px solid #f59e0b; outline-offset: 2px; }
  #toggle { display: none; font: inherit; padding: 8px 12px; border: 1px solid #c3c8d1; border-radius: 8px; background: #fff; }
  main { padding: 20px 16px; }
  main:focus { outline: none; }

  /* phones: the list becomes a panel under the bar, opened by the button */
  @media (max-width: 600px) {
    .bar { flex-wrap: wrap; }
    #toggle { display: block; margin-left: auto; }
    .bar nav { flex-basis: 100%; margin: 0; }
    #menu { display: none; flex-direction: column; gap: 0; }
    #menu.open { display: flex; }
    #menu a { display: block; padding: 12px 4px; border-top: 1px solid #eef0f3; }
  }
</style>
</head>
<body>
<a class="skip" href="#content">Skip to main content</a>

<header class="bar">
  <a class="logo" href="#top">Acme</a>
  <button id="toggle" type="button" aria-expanded="false" aria-controls="menu">Menu</button>
  <nav aria-label="Main">
    <ul id="menu">
      <li><a href="#guides" aria-current="page">Guides</a></li>
      <li><a href="#api">API</a></li>
      <li><a href="#pricing">Pricing</a></li>
      <li><a href="#contact">Contact</a></li>
    </ul>
  </nav>
</header>

<main id="content" tabindex="-1">
  <h1>Guides</h1>
  <p>Press Tab once: the skip link appears. Press Enter and focus lands here, past the menu.</p>
  <p>Make the window narrow, or open this on a phone, and the links fold into the Menu button.</p>
</main>

<script>
  const toggle = document.getElementById('toggle');
  const menu = document.getElementById('menu');

  function setOpen(open) {
    toggle.setAttribute('aria-expanded', String(open)); // screen readers announce expanded / collapsed
    menu.classList.toggle('open', open);
  }

  toggle.addEventListener('click', () => setOpen(toggle.getAttribute('aria-expanded') !== 'true'));

  // Escape closes the open menu and puts focus back on the button
  document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape' && toggle.getAttribute('aria-expanded') === 'true') {
      setOpen(false);
      toggle.focus();
    }
  });
</script>
</body>
</html>
Press Tab to reveal the skip link. Narrow the window to fold the menu into a button.

The parts, in order:

  1. Skip link. <a class="skip" href="#content"> placed off screen, moved into view with .skip:focus. Hiding it with display: none would also make it unfocusable.
  2. Target. <main id="content" tabindex="-1">. The tabindex lets main take focus when the link is followed. tabindex explains the -1 value.
  3. Labelled nav. <nav aria-label="Main"> with a list and aria-current on the active link.
  4. Menu button. Shown under 600 pixels. It carries aria-expanded and aria-controls, and the script flips aria-expanded on every click. Escape closes it and returns focus to the button.

Keep the button outside the <nav> or inside it; either works. What matters is that the button is a real <button> and reports its state.

When it does not work

What you see Cause Fix
The landmark list shows "navigation" twice Two navs with no label aria-label on each, with different names
The landmark list is long and noisy A nav around every group of links Keep nav for major blocks, leave footer and share links in plain lists
The current page is not announced Only a CSS class marks it Add aria-current="page" and style that
The skip link never appears Hidden with display: none, or no :focus style Move it off screen and bring it back on :focus
Enter on the skip link does nothing No element has the matching id Give <main> the id the link points to
The menu button does not say if it is open No aria-expanded Set it to "false" and flip it in the click handler
The label is read as "Main navigation navigation" The word is in the label Use aria-label="Main"

Landmarks and focus order are easier to check than to describe. A screenshot shows neither, and an .html attachment may open as plain code on a phone.

To send a working page, paste it into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can tab through the menu and open it themselves. If you change the code later, the same link shows the new version.

Questions people ask

What is the difference between nav and div?

A div has no meaning. A nav tells the browser that the block holds major navigation links, and the browser exposes it to assistive technology as a navigation landmark. Both look the same until you add CSS.

Does the nav tag need a ul inside it?

No. The HTML standard allows any flow content inside nav, including a paragraph of links. A ul or ol is still the usual choice, because a screen reader then announces the number of items.

Can a page have more than one nav element?

Yes. A site menu, a table of contents and a pagination bar can each be a nav. Once there are two or more, give each one a distinct aria-label, such as "Main" and "Breadcrumb", so they can be told apart.

Should footer links go in a nav?

Not necessarily. The HTML standard says the footer element alone is enough for a short list of links such as terms, privacy and contact. Use nav in the footer only when it repeats the site's main navigation.

Is role="navigation" on a div the same as nav?

It gives the div the same landmark role. The nav element does it without an extra attribute, so it is the simpler choice when you can change the markup.

Keep reading