Building a Responsive Navigation Bar with Flexbox and CSS Grid

A navigation bar is the first thing users see when they land on a site. If it looks cramped on a phone or stretches awkwardly on a widescreen, you’ve already lost half the battle for attention. In 2024, with a sea of devices ranging from 320‑pixel phones to 4K monitors, a responsive nav isn’t a nice‑to‑have—it’s a must. Let’s walk through a clean, modern approach that blends Flexbox’s simplicity with CSS Grid’s power, and end up with a bar that works everywhere without a single JavaScript hack.

Why Flexbox and Grid Together?

Both Flexbox and CSS Grid are layout modules, but they shine in different scenarios.

  • Flexbox is one‑dimensional. It excels at aligning items along a single row or column, making it perfect for the typical “logo on the left, links on the right” pattern.
  • CSS Grid is two‑dimensional. It lets you define rows and columns simultaneously, which becomes handy when you need to rearrange the whole navigation structure for smaller screens.

By starting with Flexbox for the desktop layout and then switching to Grid for the mobile breakpoint, we get the best of both worlds: minimal code, clear intent, and smooth transitions.

The HTML Skeleton

First, let’s set up a semantic markup. I like to keep the nav inside a <header> so screen readers can quickly locate the site’s primary navigation.

<header class="site-header">
  <nav class="main-nav" aria-label="Primary">
    <a href="/" class="logo">CodeCraft Hub</a>
    <ul class="nav-list">
      <li><a href="/tutorials">Tutorials</a></li>
      <li><a href="/blog">Blog</a></li>
      <li><a href="/about">About</a></li>
      <li><a href="/contact">Contact</a></li>
    </ul>
    <button class="nav-toggle" aria-expanded="false" aria-controls="nav-list">
      ☰
    </button>
  </nav>
</header>

A few notes:

  • The <ul> holds the navigation links – this is the most accessible pattern.
  • The toggle button will appear only on small screens; it’s hidden by default with CSS.
  • I’ve added aria-expanded and aria-controls to keep it screen‑reader friendly. (We’ll add a tiny bit of JavaScript later just to flip the attribute – no heavy frameworks needed.)

Desktop Layout with Flexbox

On larger viewports we want the logo on the left, the list of links centered or right‑aligned, and the toggle button hidden. Here’s the Flexbox recipe:

/* Base styles */
.site-header {
  background: #0d1117;
  color: #c9d1d9;
}

/* Flex container */
.main-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  max-width: 1200px;
  margin: 0 auto;
  padding: 0.5rem 1rem;
}

/* Logo */
.logo {
  font-size: 1.5rem;
  font-weight: bold;
  text-decoration: none;
  color: inherit;
}

/* Nav list */
.nav-list {
  display: flex;
  gap: 1.5rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Links */
.nav-list a {
  text-decoration: none;
  color: inherit;
  font-weight: 500;
}

/* Hide toggle on desktop */
.nav-toggle {
  display: none;
}

What’s happening?

  • display: flex on .main-nav creates a horizontal line.
  • justify-content: space-between pushes the logo to the far left and the nav list to the far right.
  • The .nav-list itself is also a flex container, giving us even spacing (gap) between each link without extra margins.

At this point, the bar looks crisp on a laptop or desktop. Resize the window and you’ll notice the links start to squish – that’s where the responsive switch comes in.

Switching to Grid for Mobile

When the viewport drops below, say, 768 px, we want the navigation to stack: logo on top, toggle button visible, and the menu items either hidden or displayed in a vertical list. Grid makes this rearrangement straightforward.

@media (max-width: 768px) {
  .main-nav {
    display: grid;
    grid-template-areas:
      "logo toggle"
      "menu  menu";
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: 0.5rem;
  }

  .logo {
    grid-area: logo;
  }

  .nav-toggle {
    grid-area: toggle;
    display: block;
    background: none;
    border: none;
    font-size: 1.5rem;
    color: inherit;
    cursor: pointer;
  }

  .nav-list {
    grid-area: menu;
    display: none;               /* hidden by default */
    flex-direction: column;     /* stack links vertically */
    gap: 0.75rem;
    background: #161b22;
    padding: 1rem;
    border-radius: 4px;
  }

  .nav-list.show {
    display: flex;               /* show when toggle is active */
  }
}

Key points:

  • grid-template-areas lets us name sections of the layout. The logo and toggle share the first row, while the menu occupies the second row spanning both columns.
  • The .nav-list switches from a horizontal flex row to a vertical column (flex-direction: column) when displayed.
  • We hide the list by default (display: none) and reveal it with a .show class that JavaScript will toggle.

The Tiny JavaScript Toggle

Even though the post is about CSS, a minimal script is required to make the hamburger button functional. Keep it vanilla – no dependencies.

document.addEventListener('DOMContentLoaded', function () {
  const toggle = document.querySelector('.nav-toggle');
  const menu = document.querySelector('.nav-list');

  toggle.addEventListener('click', function () {
    const expanded = toggle.getAttribute('aria-expanded') === 'true' || false;
    toggle.setAttribute('aria-expanded', !expanded);
    menu.classList.toggle('show');
  });
});

What this does:

  1. Waits for the DOM to load.
  2. Finds the button and the list.
  3. On click, flips aria-expanded (helps assistive tech) and toggles the .show class, which switches the menu’s display from none to flex.

That’s it – no jQuery, no React, just plain JavaScript.

Polishing the Experience

A few extra touches make the nav feel professional:

  • Transition – add a subtle fade when the menu appears.
.nav-list {
  transition: opacity 0.3s ease;
  opacity: 0;
}
.nav-list.show {
  opacity: 1;
}
  • Focus styles – ensure keyboard users can see which link is active.
.nav-list a:focus {
  outline: 2px solid #58a6ff;
  outline-offset: 2px;
}
  • Dark mode awareness – if your site supports a dark theme, use CSS variables for colors so the nav adapts automatically.
:root {
  --bg: #0d1117;
  --text: #c9d1d9;
}
[data-theme="dark"] {
  --bg: #0d1117;
  --text: #c9d1d9;
}
.site-header { background: var(--bg); color: var(--text); }

Testing on Real Devices

I can’t stress enough how valuable real‑device testing is. Emulators are great, but they sometimes hide subtle touch‑target issues. Grab a phone, open the dev tools, and verify:

  • The toggle button is large enough to tap comfortably (at least 44 px square).
  • Links have enough spacing to avoid accidental clicks.
  • The transition feels snappy, not laggy.

If anything feels off, tweak the gap or padding values – Flexbox and Grid are forgiving, so small adjustments go a long way.

Wrap‑Up

Building a responsive navigation bar doesn’t have to be a maze of media queries and JavaScript gymnastics. By letting Flexbox handle the straightforward desktop row and handing the mobile rearrangement over to CSS Grid, you keep the code readable and the intent clear. The result is a navigation component that feels native on any screen, stays accessible, and requires only a handful of lines of CSS and a tiny script.

Give it a spin in your next project, and you’ll see why I keep returning to this pattern for every new site I launch. Happy coding!

Reactions