Responsive Navigation Bar with a Hamburger Menu

Build a navigation bar that shows links in a row on desktops and collapses into an accessible hamburger menu on phones, with CSS and a few lines of JavaScript.

Lesson 39 of 49Intermediate15 min read

A desktop navigation bar with six links fits comfortably across a 1280-pixel screen. On a 360-pixel phone, those same links either wrap onto three messy lines or overflow off the edge. The standard solution is the hamburger menu: on small screens the links are hidden behind a button with three lines, and tapping it opens the menu.

Building one well takes a little care. The menu must open and close with a tap, be usable with the keyboard, tell screen readers whether it's open, and switch back to a normal row of links on larger screens. You'll build it mobile first: the collapsed phone menu is the default, and a media query turns it into a horizontal bar on wider screens.

This lesson shows two versions. The first uses a real <button> and three lines of JavaScript to toggle the menu and update aria-expanded, which is the most accessible approach. The second uses <details> and <summary> for a menu that works with no JavaScript at all. Both build on the lists and navbars lesson.

The HTML structure

HTML
<header class="site-header">
  <a class="logo" href="/">Brand</a>
  <button class="menu-toggle" aria-expanded="false" aria-controls="menu">Menu</button>
  <nav id="menu" class="menu">
    <ul>...links...</ul>
  </nav>
</header>
  • The toggle is a <button>, so it works with Enter and Space and is announced as a button.
  • aria-expanded tells screen readers whether the menu is open; JavaScript switches it between "true" and "false".
  • aria-controls links the button to the menu it opens.
  • Give the button a visible label or an aria-label such as "Menu", not just three lines.

Mobile-first CSS

CSS
.menu { display: none; }                                  /* phones: hidden */
.menu-toggle[aria-expanded="true"] + .menu { display: block; } /* open */

@media (min-width: 768px) {
  .menu-toggle { display: none; }                         /* no button needed */
  .menu { display: block; }                               /* always visible */
  .menu ul { display: flex; gap: 8px; }                   /* a row of links */
}

Styling the open state from the aria-expanded attribute keeps the visual state and the accessibility state in sync: one attribute controls both. If you prefer, toggle a class such as .is-open, but then update aria-expanded too.

The three-line JavaScript toggle

JavaScript
const btn = document.querySelector('.menu-toggle');
btn.addEventListener('click', () => {
  const open = btn.getAttribute('aria-expanded') === 'true';
  btn.setAttribute('aria-expanded', String(!open));
});

That's all the JavaScript a basic hamburger menu needs. For a polished version, also close the menu when the user presses Esc and when they choose a link. You'll learn event listeners in JavaScript events.

Drawing the hamburger icon

ApproachHow
Inline SVGThree <line> or <rect> elements; easiest to make sharp and animate
CSS linesOne <span> plus ::before and ::after, each a 2px-tall bar
TextThe word "Menu", which is clearer for many users than an icon alone

To morph the icon into an X when open, rotate the top and bottom bars 45 degrees in opposite directions and hide the middle one, using transforms and a short transition.

CSS Responsive Navbar Syntax

Responsive navbar

.site-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; }
.menu { display: none; flex-basis: 100%; }
.menu-toggle[aria-expanded="true"] + .menu { display: block; }
.menu a { display: block; padding: 12px; }
@media (min-width: 768px) {
  .menu-toggle { display: none; }
  .menu { display: block; flex-basis: auto; }
  .menu ul { display: flex; }
}

CSS Responsive Navbar Summary

  • On small screens, hide the links behind a toggle button; show them as a row on larger screens.
  • Use a real <button> with aria-expanded and aria-controls.
  • Style the open state from [aria-expanded="true"] so visuals and accessibility stay in sync.
  • A few lines of JavaScript flip aria-expanded; <details> works with no JavaScript.
  • Keep tap targets large and respect prefers-reduced-motion.

CSS Responsive Navbar Examples

Each example runs in the page. Edit the code and press Run to see what changes.

Example 1

A hamburger menu with a toggle button

Make the result panel narrow and press the button. On wide panels the links show in a row.

.site-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; padding: 8px 12px; background: #0f172a; font-family: sans-serif; }
.logo { color: white; font-weight: 700; text-decoration: none; }

.menu-toggle { width: 44px; height: 44px; border: 0; background: transparent; cursor: pointer; display: grid; place-items: center; }
.bars, .bars::before, .bars::after { display: block; width: 22px; height: 2px; background: white; border-radius: 2px; transition: transform 0.2s, background 0.2s; }
.bars { position: relative; }
.bars::before, .bars::after { content: ""; position: absolute; left: 0; }
.bars::before { top: -7px; } .bars::after { top: 7px; }
.menu-toggle[aria-expanded="true"] .bars { background: transparent; }
.menu-toggle[aria-expanded="true"] .bars::before { transform: translateY(7px) rotate(45deg); }
.menu-toggle[aria-expanded="true"] .bars::after { transform: translateY(-7px) rotate(-45deg); }

.menu { display: none; flex-basis: 100%; }
.menu-toggle[aria-expanded="true"] + .menu { display: block; }
.menu ul { list-style: none; margin: 0; padding: 0; }
.menu a { display: block; padding: 12px 8px; color: #e2e8f0; text-decoration: none; border-radius: 6px; }
.menu a:hover { background: #1e293b; }

@media (min-width: 600px) {
  .menu-toggle { display: none; }
  .menu { display: block; flex-basis: auto; }
  .menu ul { display: flex; gap: 4px; }
}
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 4A 44px button is easy to tap.
  2. Lines 5 to 8One span and two pseudo-elements draw the three bars.
  3. Lines 9 to 11When open, the middle bar fades and the others rotate into an X.
  4. Lines 13 to 14The menu is hidden until the button says it's expanded, and takes a full row when open.
  5. Lines 19 to 23From 600px, the button disappears and the links sit in a row.
Example 2

A menu with no JavaScript

.bar { display: flex; justify-content: space-between; align-items: start; padding: 10px 12px; background: #ecfdf5; font-family: sans-serif; }

.nav summary {
  list-style: none;
  cursor: pointer;
  padding: 8px 14px;
  border: 1px solid #16a34a;
  border-radius: 8px;
  font-weight: 600;
}
.nav summary::-webkit-details-marker { display: none; }
.nav[open] summary { background: #16a34a; color: white; }

.nav nav { display: grid; margin-top: 6px; }
.nav a { padding: 10px; color: #14532d; text-decoration: none; }
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Line 4list-style: none removes the default triangle.
  2. Line 11Safari needs this line to hide its triangle too.
  3. Line 12The [open] attribute styles the open state.
  4. Line 14The links stack beneath the button.
Example 3

Respecting reduced motion

.panel {
  display: grid;
  max-height: 0;
  overflow: hidden;
  font-family: sans-serif;
}
.toggle[aria-expanded="true"] + .panel { max-height: 200px; }
.panel a { padding: 10px; }

@media (prefers-reduced-motion: no-preference) {
  .panel { transition: max-height 0.3s ease; }
}
ResultEdit the code, then press Run or Ctrl+Enter
Line-by-line explanation
  1. Lines 3 to 4The panel is collapsed until opened.
  2. Line 7Opening it sets a max height large enough for the links.
  3. Lines 10 to 12The slide animation only runs for people who haven't asked for reduced motion.

Common CSS Responsive Navbar Mistakes

A div as the toggle

Don't
/* <div class="menu-toggle" onclick="...">☰</div> */
Do
/* <button class="menu-toggle" aria-expanded="false" aria-label="Menu">☰</button> */

A div can't be focused or pressed with the keyboard, and screen readers don't know it's a control.

Hiding the menu on desktop by accident

Don't
.menu { display: none; }
.menu.open { display: block; } /* desktop never shows links */
Do
.menu { display: none; }
.menu.open { display: block; }
@media (min-width: 768px) { .menu { display: block; } }

Forgetting to update aria-expanded

Don't
/* JavaScript only toggles .open; aria-expanded stays "false" */
.menu.open { display: block; }
Do
.menu-toggle[aria-expanded="true"] + .menu { display: block; }

Driving the style from the attribute guarantees screen readers hear the right state.

CSS Responsive Navbar in Real Projects

Navigation on real sites

Almost every mobile site uses a collapsible menu. Large sites such as the BBC and GOV.UK use a button labelled "Menu" with aria-expanded, and test it with screen readers. E-commerce apps often slide the menu in from the side as a drawer. Frameworks such as Bootstrap ship a "navbar-toggler" component built on exactly these ideas.

CSS Responsive Navbar Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Show the menu when the toggle button says it's open.

.menu-toggle[aria-expanded=""] + .menu {
  display: block;
}
Multiple choiceeasy

Which element should open and close a hamburger menu?

Multiple choicemedium

In a mobile-first navbar, what does the min-width media query do?

Multiple choicemedium

Which HTML gives a collapsible menu with no JavaScript?

Fix the bugmedium

The menu should be hidden by default, shown when the button has aria-expanded="true", and always shown from 600px. Fix the CSS.

.mn { display: block; }
.tg[aria-expanded="false"] + .mn { display: block; }
@media (max-width: 600px) { .mn { display: none; } }
ResultEdit the code, then press Run or Ctrl+Enter
Checks
  • Open menu is visible
  • Hidden by default
  • Always shown on wide screens

CSS Responsive Navbar Quiz

5 questions to check what you learned.

  1. What does aria-expanded="true" tell screen readers?

    • The menu is large
    • The control's menu is open
    • The page is loading
    • The button is disabled
  2. How do CSS hamburger icons usually draw three bars?

    • One element plus ::before and ::after
    • Three images
    • Three buttons
    • A border-image
  3. Why show the word "Menu" next to or instead of the icon?

    • It loads faster
    • Icons aren't allowed
    • It's needed for SEO
    • Some users don't recognise the three-line icon
  4. What should happen when the user presses Esc with the menu open?

    • Nothing
    • The page reloads
    • The menu closes
    • The menu moves
  5. How can you stop the menu animation for people who prefer less motion?

    • Only add the transition inside @media (prefers-reduced-motion: no-preference)
    • @media (motion: off)
    • animation: none on body
    • It can't be detected

CSS Responsive Navbar Challenge

Make the navbar responsive

  1. Hide .links by default with display: none
  2. Show .links (display: block) when .toggle has aria-expanded="true"
  3. In the JavaScript, flip aria-expanded between "true" and "false" on click
  4. From min-width: 700px, hide .toggle and show .links as a flex row
ResultEdit the code, then press Run or Ctrl+Enter
Checks
  • Links hidden by default
  • Click opens the menu
  • Click again closes it
  • Open state shows the links
  • Desktop row in a media query