How to Create a Responsive Navbar in CSS

This recipe builds a navbar that shows a horizontal menu on wide screens and collapses into a toggleable hamburger menu on small screens, using flexbox and a media query.

What responsive means for a navbar

The common pattern is: above a chosen breakpoint, links sit in a horizontal row; below it, the links hide and a hamburger button takes their place. Tapping the hamburger reveals the links again, usually as a full-width panel. Getting this right needs three pieces working together: a CSS layout for the wide view, a media query that swaps to the narrow view, and a small script that toggles the narrow menu open and closed.

The full example

The markup stays plain: a nav element, an unordered list of links, and a real button for the toggle (not a div - buttons are focusable and clickable with the keyboard for free). The CSS lays the nav out with flexbox, and a media query hides the link list and reveals the button below the breakpoint. A few lines of JavaScript flip a class on click.

Example

<!DOCTYPE html>
<html>
<head>
<style>
  * { box-sizing: border-box; }
  body { margin: 0; font-family: sans-serif; }

  .navbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 16px 24px;
    background: #1f2937;
    color: white;
  }

  .brand { font-weight: bold; font-size: 1.2rem; }

  .nav-toggle {
    display: none;
    background: none;
    border: none;
    color: white;
    font-size: 1.5rem;
    cursor: pointer;
  }

  .nav-links {
    display: flex;
    gap: 20px;
    list-style: none;
    margin: 0;
    padding: 0;
  }

  .nav-links a {
    color: white;
    text-decoration: none;
  }

  @media (max-width: 700px) {
    .nav-toggle { display: block; }

    .nav-links {
      display: none;
      flex-direction: column;
      width: 100%;
      position: absolute;
      top: 60px;
      left: 0;
      background: #1f2937;
      padding: 16px 24px;
    }

    .nav-links.open {
      display: flex;
    }
  }
</style>
</head>
<body>
  <nav class='navbar'>
    <span class='brand'>MySite</span>
    <ul class='nav-links' id='navLinks'>
      <li><a href='#'>Home</a></li>
      <li><a href='#'>About</a></li>
      <li><a href='#'>Services</a></li>
      <li><a href='#'>Contact</a></li>
    </ul>
    <button class='nav-toggle' id='navToggle' aria-expanded='false' aria-controls='navLinks'>&#9776;</button>
  </nav>

  <script>
    const toggle = document.getElementById('navToggle');
    const links = document.getElementById('navLinks');
    toggle.addEventListener('click', () => {
      const isOpen = links.classList.toggle('open');
      toggle.setAttribute('aria-expanded', isOpen);
    });
  </script>
</body>
</html>
Note: A button, not a div or span, should always be the toggle element - it is reachable with Tab and activates with Enter or Space without any extra JavaScript.

How the breakpoint works

Above the breakpoint, .nav-links is a normal flex row and .nav-toggle stays hidden - there is nothing to toggle because the links are already visible. At or below the breakpoint, the media query hides .nav-links by default and shows the button instead; clicking the button adds an open class that the same media query turns back into display: flex. Nothing changes above the breakpoint because the open class only has an effect inside the query.

Example

<!DOCTYPE html>
<html>
<head>
<style>
/* Above 700px: links show as a row, the hamburger is hidden */
.nav-links { display: flex; gap: 20px; }
.nav-toggle { display: none; }

/* At or below 700px: hide the row, show the hamburger instead */
@media (max-width: 700px) {
  .nav-toggle { display: block; }
  .nav-links { display: none; }
  .nav-links.open { display: flex; flex-direction: column; }
}
</style>
</head>
<body>

<nav class="navbar">
  <ul class="nav-links">
    <li><a href="#">Home</a></li>
    <li><a href="#">About</a></li>
    <li><a href="#">Services</a></li>
    <li><a href="#">Contact</a></li>
  </ul>
  <button class="nav-toggle">&#9776;</button>
</nav>

</body>
</html>
  • Keep aria-expanded on the button in sync with the open or closed state, so screen readers announce the change.
  • Point aria-controls at the id of the menu the button toggles.
  • Keep tap targets at least about 44px tall on mobile so links are easy to hit with a thumb.

Two common variations are worth knowing. A pure-CSS checkbox hack achieves the same open and close behavior with a hidden checkbox and a label styled as the button, avoiding JavaScript entirely, but it is semantically odd and generally less accessible. A slide-in side panel is the same idea with a transform-based animation instead of a simple display toggle.

ApproachHow it worksTrade-off
JavaScript + classList.toggle (this recipe)A button click toggles a class that a media-query rule shows or hidesA few lines of script, but easy to add aria-expanded and keyboard support
Checkbox hack (pure CSS)A hidden checkbox's :checked state, paired with a label, reveals the menu through a sibling selectorNo JavaScript at all, but semantically odd and less accessible
Framework state (React, Vue, etc.)A boolean in component state toggles a class or a conditional renderFits naturally into an app that already uses a framework; overkill for a static page

Frequently Asked Questions

How do I make a navbar responsive without JavaScript?
Use a media query to switch the flex direction from row to column below your breakpoint. For a genuine toggle you need a little JavaScript, or a hidden checkbox paired with a label, which flips the menu open using CSS alone.
What is the best breakpoint for a mobile navbar?
Around 768px is the common choice, but pick the width where your own links start to wrap rather than copying a number. Resize the browser until the layout breaks, and set the breakpoint there.