For good reason
A responsive navbar with Flexbox should be simple to scan, easy to operate on mobile, and friendly to screen readers. This pattern gives you a clean, accessible header that collapses to a tidy mobile menu with a small, smooth animation. No libraries. Minimal CSS. A dash of JavaScript to toggle state correctly.
If you’re not familiar with the base concepts of Flexbox, I’d recommend checking out my CSS Grid vs Flexbox guide first.
There’s a lot to digest here. But, if you want to see the finished, functioning Codepen rather than assembling to test yourself, click here.
What we’re building

Desktop: logo at left, links at right, items aligned with Flexbox
Mobile: a toggle button exposes a vertical menu with a gentle open animation
Accessibility: proper landmarks, aria attributes, focus styles, and keyboard support
Performance: zero libraries, very little JS
The HTML
Copy this into your header template. Replace the src placeholder with your real logo when you are ready. You can discard everything below the closing </header> tag if you only require the Nav.
<!-- Skip link for keyboard users -->
<a class="skip-link" href="#main">Skip to content</a>
<header class="site-header">
<div class="nav" data-js="nav">
<a class="nav__brand" href="#" aria-label="Homepage">
<!-- Simple <img> placeholder, swap the src for your logo file -->
<img
class="nav__brand-img"
src="https://placehold.co/800x400/fcd21d/111111.png?text=Your+Logo"
alt="Your logo"
width="800"
height="400"
decoding="async"
/>
</a>
<!-- Mobile toggle button with accessible labels -->
<button
class="nav__toggle"
type="button"
aria-controls="primary-menu"
aria-expanded="false"
aria-label="Open menu"
>
<svg class="nav__icon" viewBox="0 0 24 24" aria-hidden="true">
<path
d="M4 7h16M4 12h16M4 17h16"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
</button>
<!-- Primary nav -->
<nav class="nav__menu" id="primary-menu" aria-label="Primary">
<ul class="nav__list">
<li><a href="#services">Services</a></li>
<li><a href="#work">Work</a></li>
<li><a href="#blog">Blog</a></li>
<li><a href="#about">About</a></li>
<li class="cta"><a href="#contact">Contact</a></li>
</ul>
</nav>
</div>
</header>
<main id="main" class="container">
<section class="hero">
<h1>Build a responsive navbar with Flexbox</h1>
<p>
Accessible, mobile first, and fast. This pattern keeps markup semantic, adds helpful aria attributes, and uses a small animation for a polished mobile experience.
</p>
</section>
<section class="demo-cards">
<article class="card">
<h3>Card one</h3>
<p>Use these to show layout integrity under scroll.</p>
</article>
<article class="card">
<h3>Card two</h3>
<p>Escape closes the menu and returns focus.</p>
</article>
<article class="card">
<h3>Card three</h3>
<p>No layout shift when toggling the menu.</p>
</article>
</section>
</main>
<footer class="footer"><div class="footer-ajm"><p>A contribution by <a href="https://ajmmedia.co.uk/">AJM Media 2025.</a></p></div></footer>
What’s happening here
The skip link lets keyboard users jump straight to main content.
The brand is a single anchor with an
<img>logo you can swap for your own file.The button controls the menu and uses
aria-controls,aria-expanded, and anaria-label.The nav contains a simple list of links. No dropdowns yet, just a solid base.
The CSS
Branding background color set to #03000c. Flexbox for desktop alignment. Mobile menu animates open with scaleY and opacity.
:root {
/* Brand and layout tokens */
--bg: #03000c;
--muted: #cdd3df;
--accent: #fcd21d;
--wrap: 1100px;
--ease: cubic-bezier(.2, .6, .2, 1);
--speed: 200ms;
--ink: #ffffff;
/* Nav logo & cta root css */
--nav-pill-h: 36px;
--nav-pill-r: 8px;
--nav-pill-pad-x: 14px;
}
/* Base resets and typography */
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
font-family: Inter, system-ui, -apple-system, Roboto, sans-serif;
}
body {
color: #141823;
background: var(--bg);
}
/* Content container */
.container {
max-width: var(--wrap);
margin: 0 auto;
padding: 0 .75rem;
}
/* Keyboard skip link */
.skip-link {
position: absolute;
left: -9999px;
top: auto;
background: #111;
color: #fff;
padding: .5rem .75rem;
}
.skip-link:focus {
left: .75rem;
top: .75rem;
z-index: 1000;
}
/* Sticky header shell */
.site-header {
position: sticky;
top: 0;
z-index: 100;
background: var(--bg);
color: var(--ink);
}
/* Navbar frame */
.nav {
display: flex;
align-items: center;
justify-content: space-between;
max-width: var(--wrap);
margin: 0 auto;
gap: .5rem;
padding: 0.75rem;
}
/* Brand link/logo image */
.nav__brand {
display: inline-flex;
align-items: center;
gap: .5rem;
color: var(--ink);
text-decoration: none;
padding: 0;
}
.nav__brand-img {
display: block;
height: var(--nav-pill-h);
width: auto;
}
/* Toggle button for mobile menu */
.nav__toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: transparent;
border: 1px solid rgba(255, 255, 255, .2);
color: var(--ink);
cursor: pointer;
}
.nav__toggle:hover {
background: rgba(255, 255, 255, .06);
}
.nav__icon {
width: 22px;
height: 22px;
}
/* Navigation list */
.nav__menu {
/* shown on desktop via media query, animated on mobile */
}
.nav__list {
display: flex;
align-items: center;
gap: 1rem;
list-style: none;
margin: 0;
padding: 0;
}
/* Menu items use the same pill metrics as the logo */
.nav__list a {
display: inline-block;
line-height: var(--nav-pill-h);
padding: 0 var(--nav-pill-pad-x);
margin: 0 .5rem;
color: var(--muted);
text-decoration: none;
}
.nav__list a:hover,
.nav__list a:focus {
color: var(--ink);
background: rgba(255, 255, 255, .06);
outline: 0;
}
/* CTA link styling */
.nav__list .cta a {
display: inline-block;
line-height: var(--nav-pill-h);
padding: 0 var(--nav-pill-pad-x);
margin: 0 .5rem;
color: var(--muted);
text-decoration: none;
background: var(--accent);
color: #000;
}
.nav__list .cta a:hover {
filter: brightness(.95);
}
.footer {
position: fixed;
z-index: 10;
right: 0;
bottom: 0;
left: 0;
padding: 0 1rem;
}
.footer-ajm {
font-size: .9375rem;
color: white;
}
.footer a {
color: var(--accent);
text-decoration: none;
}
/* Mobile menu behaviour and animation */
@media (max-width: 860px) {
.nav {
padding: .6rem .9rem;
}
.nav__toggle {
display: inline-flex;
}
.nav__menu {
position: absolute;
left: 0;
right: 0;
top: 100%;
background: var(--bg);
/* closed state */
transform: scaleY(0);
transform-origin: top;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition-property: transform, opacity, visibility;
transition-duration: var(--speed);
transition-timing-function: var(--ease);
}
.nav[data-state="open"] .nav__menu {
/* open state */
transform: scaleY(1);
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.nav__list {
flex-direction: column;
align-items: stretch;
gap: .75rem;
padding: .5rem .5rem 1rem .5rem;
}
.container {
padding: 0 .9rem;
}
.footer {
background: var(--bg);
}
}
/* Desktop menu visibility */
@media (min-width: 861px) {
.site-header {
border-bottom: 1px solid rgba(255, 255, 255, 0.4);
}
.nav__toggle {
display: none;
}
.nav__menu {
display: block;
}
}
/* Demo content styling */
.hero {
color: var(--ink);
padding: 60px 0;
}
.hero h1 {
margin: .25rem 0 0;
font-size: clamp(1.6rem, 2.4vw, 3.125rem);
color: #fff;
font-weight: 400;
letter-spacing: -0.15rem;
}
.hero p {
color: #FFFFFF99;
max-width: 70ch;
text-wrap: balance;
}
.demo-cards {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.card {
color: #FFFFFF99;
border: 1px solid rgba(255, 255, 255, 0.4);
padding: 1rem;
}
.card h3 {
margin: .2rem 0 .4rem;
color: #fff;
font-weight: 400;
}
@media (max-width: 900px) {
.demo-cards {
grid-template-columns: 1fr 1fr;
}
}
@media (max-width: 600px) {
.demo-cards {
grid-template-columns: 1fr;
}
}
}What’s happening here
Flexbox aligns brand and links; a breakpoint switches to a stacked menu.
The mobile menu is absolutely positioned and uses
scaleYandopacityfor a quick, smooth open that avoids layout shift.Focus styles are visible and a reduced motion preference disables transitions.
The Javascript
A small (believe it or not) script to toggle the menu, update aria-expanded, close on Escape or outside click, and return focus to the toggle when closing.
(function () {
// Run only after DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
function init() {
var nav = document.querySelector('[data-js="nav"]');
if (!nav) return;
var toggle = nav.querySelector('.nav__toggle');
var menu = nav.querySelector('#primary-menu');
if (!toggle || !menu) return;
var firstLink = menu.querySelector('a');
function openMenu() {
nav.setAttribute('data-state', 'open');
toggle.setAttribute('aria-expanded', 'true');
toggle.setAttribute('aria-label', 'Close menu');
if (firstLink) { try { firstLink.focus({ preventScroll: true }); } catch(e){} }
document.addEventListener('click', onDocClick);
document.addEventListener('keydown', onKeyDown);
}
function closeMenu() {
nav.removeAttribute('data-state');
toggle.setAttribute('aria-expanded', 'false');
toggle.setAttribute('aria-label', 'Open menu');
try { toggle.focus({ preventScroll: true }); } catch(e){}
document.removeEventListener('click', onDocClick);
document.removeEventListener('keydown', onKeyDown);
}
function onDocClick(e) {
if (!nav.contains(e.target)) closeMenu();
}
function onKeyDown(e) {
if (e.key === 'Escape' || e.key === 'Esc') closeMenu();
}
// Prevent the opening click from bubbling to the document listener
toggle.addEventListener('click', function (e) {
e.stopPropagation();
var isOpen = nav.getAttribute('data-state') === 'open';
isOpen ? closeMenu() : openMenu();
});
// Close after navigating via a link (useful for one-pagers)
menu.addEventListener('click', function (e) {
if (e.target.closest('a') && nav.getAttribute('data-state') === 'open') {
closeMenu();
}
});
}
})();
Accessibility checklist
The navigation lives in a
<nav>witharia-label="Primary"The toggle button has
aria-controls,aria-expanded, and a meaningfularia-labelThe open state is announced because
aria-expandedupdates on toggleEscape closes the menu and returns focus to the toggle, as in my accessible email capture modal
Focus styles are visible and not removed
A Skip to content link is available before the header
Need a clean navbar with flexbox implementing in your site?
Or have questions regarding anything in this blog?
