CSSNavbar & Header Patterns

Navbar & Header Patterns

Almost every site needs a navigation bar, and almost every navigation bar needs to do the same handful of things: line up a logo and some links horizontally, stay usable on a phone-sized screen, and often stay visible while the page scrolls. None of that requires anything exotic — Flexbox, a checkbox hack for no-JS collapsing, and position: sticky cover the vast majority of real navbars. This page collects the recipes.

A horizontal nav with Flexbox

The bread-and-butter navbar layout is a flex container with the logo on one side and the link list on the other. justify-content: space-between does the heavy lifting — it pushes the first and last flex items to opposite ends and lets Flexbox handle the rest.

HTML
<header class="navbar">
  <a class="navbar__logo" href="/">Brand</a>
  <nav>
    <ul class="navbar__links">
      <li><a href="/">Home</a></li>
      <li><a href="/docs">Docs</a></li>
      <li><a href="/pricing">Pricing</a></li>
      <li><a href="/contact">Contact</a></li>
    </ul>
  </nav>
</header>

CSS
.navbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem 1.5rem;
  background: #ffffff;
  border-bottom: 1px solid #e5e7eb;
}

.navbar__logo {
  font-weight: 700;
  font-size: 1.25rem;
  text-decoration: none;
  color: #111827;
}

.navbar__links {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.navbar__links a {
  text-decoration: none;
  color: #374151;
  font-weight: 500;
}

.navbar__links a:hover {
  color: #111827;
}
Collapsing into a hamburger menu (CSS-only)

On narrow viewports there usually isn't room for a full link list, so the classic move is to hide the links behind a "hamburger" icon. A pure-CSS version of this uses a hidden checkbox plus a <label> styled as the icon: checking the box (by clicking the label) toggles a sibling's visibility through the :checked pseudo-class and the general sibling combinator — no JavaScript required.

HTML
<header class="navbar">
  <a class="navbar__logo" href="/">Brand</a>

  <input type="checkbox" id="nav-toggle" class="navbar__toggle-input" />
  <label for="nav-toggle" class="navbar__toggle-label" aria-label="Menu">
    &#9776;
  </label>

  <nav class="navbar__menu">
    <ul class="navbar__links">
      <li><a href="/">Home</a></li>
      <li><a href="/docs">Docs</a></li>
      <li><a href="/pricing">Pricing</a></li>
      <li><a href="/contact">Contact</a></li>
    </ul>
  </nav>
</header>

CSS
.navbar__toggle-input {
  display: none;
}

.navbar__toggle-label {
  display: none;
  font-size: 1.5rem;
  cursor: pointer;
}

@media (width < 768px) {
  .navbar__toggle-label {
    display: block;
  }

  .navbar__menu {
    display: none;
    width: 100%;
    order: 3;
  }

  /* When the checkbox is checked, show the sibling menu */
  .navbar__toggle-input:checked ~ .navbar__menu {
    display: block;
  }

  .navbar {
    flex-wrap: wrap;
  }

  .navbar__links {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 1rem 0;
  }
}
Accessibility caveat
The checkbox toggle is a neat no-JS trick, but it is not a fully accessible disclosure widget on its own — it does not announce expanded/collapsed state to screen readers the way aria-expanded on a real <button> does, and keyboard users can only reach it via tab order rather than the Enter/Space activation pattern they expect from a menu button. A production navbar typically pairs a real <button aria-expanded aria-controls> with a small amount of JavaScript to toggle a class and manage focus, using the CSS above purely for the visual collapse/expand.
Sticky header on scroll

Keeping the navbar visible while the page scrolls is just position: sticky with an offset (usually top: 0) — the full mechanics are covered on the position: sticky page. The one navbar-specific detail is that a sticky header needs an opaque background and a sensible z-index, otherwise content scrolling underneath will show through or overlap it.

CSS
.navbar {
  position: sticky;
  top: 0;
  z-index: 100;
  background: #ffffff; /* opaque — content must not show through */
}
Putting it together

A complete, responsive, sticky navbar combines all three techniques: Flexbox for the row layout, the checkbox toggle for the mobile collapse, and position: sticky to keep it on screen.

CSS
.navbar {
  position: sticky;
  top: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  padding: 0.75rem 1.5rem;
  background: #ffffff;
  border-bottom: 1px solid #e5e7eb;
}

.navbar__toggle-input {
  display: none;
}

.navbar__toggle-label {
  display: none;
  font-size: 1.5rem;
  cursor: pointer;
}

@media (width < 768px) {
  .navbar__toggle-label {
    display: block;
  }

  .navbar__menu {
    display: none;
    width: 100%;
    order: 3;
  }

  .navbar__toggle-input:checked ~ .navbar__menu {
    display: block;
  }
}
Quick reference

Pattern

Core CSS

When to reach for it

Logo + links row

display: flex; justify-content: space-between

Standard desktop navbar layout

Hamburger collapse

Hidden checkbox + :checked ~ sibling combinator

Mobile menu with zero JavaScript for the visual toggle

Stays on screen

position: sticky; top: 0;

Header should remain visible while the page scrolls

  • Flexbox handles the row layout — logo and links on opposite ends.

  • The checkbox/label hack collapses the menu without JavaScript, but is not a substitute for proper ARIA attributes and keyboard handling.

  • position: sticky (recap: position: sticky) keeps the header visible on scroll — give it an opaque background and a z-index.

Note
Most production navbars end up using a small amount of JavaScript anyway — for closing the mobile menu on route change, trapping focus, or toggling aria-expanded. Treat the CSS-only techniques here as the visual foundation, not the whole accessibility story.
Next
Building overlays that sit on top of the page: [Modal & Dialog Patterns](/css/modal-patterns).