LearnAI ToolsCareerPractice BuildsPlayContact
CSSBeginner~1.5 hours

Responsive Navigation Bar

Build a navbar that adapts from desktop to a mobile hamburger menu.

FlexboxMedia QueriesPositioning

Overview

Every real website needs a navigation bar that works on both a wide desktop monitor and a narrow phone screen, and the way it adapts between the two is one of the first real responsive-design problems a CSS learner runs into. This project builds exactly that: a header with a logo and a row of links that lays itself out horizontally with Flexbox on wide screens, then collapses into a hamburger-triggered dropdown menu once the viewport gets too narrow for the full row to fit comfortably.

The interesting constraint on a CSS-only course is that a hamburger menu normally needs JavaScript to toggle open and closed. This project solves that with the classic "checkbox hack": a hidden checkbox holds the open/closed state, a `<label>` styled as the hamburger icon toggles that checkbox when clicked, and a CSS sibling selector reveals the menu whenever the checkbox is checked. No JavaScript is written anywhere in this project — the interactivity is pure CSS.

What You'll Build
  • A dark header with a logo on the left and nav links on the right, laid out with Flexbox.
  • A styled call-to-action link that stands out from the plain nav links.
  • A hamburger icon built from three `<span>` bars, hidden on desktop and shown on mobile.
  • A checkbox-hack toggle that opens and closes the mobile menu with zero JavaScript.
  • A media query that switches the whole layout from a horizontal row to a stacked dropdown.
  • A sticky header that stays pinned to the top of the page while scrolling.

Prerequisites

  • The CSS box model — margin, padding, border, and how `box-sizing` affects sizing.
  • Flexbox basics — `display: flex`, `justify-content`, `align-items`, and `flex-direction`.
  • CSS selectors — class selectors, descendant selectors, and the `:hover` and `:checked` pseudo-classes.
  • Media queries — the `@media (max-width: ...)` syntax for applying styles below a given viewport width.
  • Basic HTML — forms elements like `<input>` and `<label>`, and how a `<label for="...">` links to an `<input>`'s `id`.

Project Structure

This is a CSS project, so the HTML markup is written once and never changes — every step from here on only adds or refines CSS rules. The markup below is deliberately minimal: a `<header>` containing the logo, the hidden checkbox, its label styled as the hamburger icon, and a `<nav>` wrapping the link list. Notice the order of elements — the checkbox, its label, and the menu `<nav>` are all siblings inside `.navbar`, which is what makes the CSS sibling-selector trick in Step 4 possible.

<!-- Site header: everything the navbar needs lives inside this one element -->
<header class="site-header">
<div class="navbar">
<a href="#" class="navbar__logo">DevSite</a>
<!-- Checkbox-hack: this checkbox is never visually shown, but its checked
state is what Step 4/5's CSS uses to know whether the menu is open -->
<input type="checkbox" id="nav-toggle" class="nav-toggle">
<!-- Clicking anywhere on this label toggles the checkbox above, because
its "for" attribute matches the checkbox's "id" -->
<label for="nav-toggle" class="nav-toggle-label">
<span></span>
<span></span>
<span></span>
</label>
<!-- Must come AFTER .nav-toggle in the markup for the ~ sibling selector to work -->
<nav class="navbar__menu">
<ul class="navbar__links">
<li><a href="#" class="navbar__link">Home</a></li>
<li><a href="#" class="navbar__link">Courses</a></li>
<li><a href="#" class="navbar__link">Projects</a></li>
<li><a href="#" class="navbar__link">About</a></li>
<li><a href="#" class="navbar__link navbar__link--cta">Sign Up</a></li>
</ul>
</nav>
</div>
</header>
<!-- Some page content underneath, just so the header has something to sit above -->
<main class="page-content">
<h1>Welcome to DevSite</h1>
<p>Shrink the browser (or this preview) below tablet width to see the navbar collapse into a hamburger menu.</p>
</main>

Keep this exact markup in mind for every step below — nothing is added or removed from it later, only the CSS applied to it changes.

Step 1: Reset Defaults and Style the Header Shell

Every browser ships its own default margins, paddings, and box-sizing behavior, so the first rule in almost any CSS project is a small reset that puts every browser on the same footing. After that, we give the header its dark background and set up `.navbar` as a centered, padded container for its contents.

/* Reset default spacing so the layout starts from a predictable baseline in every browser */
* {
margin: 0; /* Browsers apply default margins to headings, lists, etc. — remove them all */
padding: 0; /* Same idea for default padding, e.g. on <ul> */
box-sizing: border-box; /* Width/height include padding and border instead of adding on top of them */
}
body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; /* Safe, native-looking font stack */
color: #1f2937; /* Dark slate for body text, easier on the eyes than pure black */
background: #f8fafc; /* Very light gray page background behind the dark header */
line-height: 1.5; /* Comfortable reading line height for paragraph text */
}
.site-header {
background: #111827; /* Dark navy-black header background, contrasts with the light page body */
color: #ffffff; /* White text/icons read clearly against the dark background */
}
.navbar {
max-width: 1100px; /* Stops the navbar content from stretching edge-to-edge on very wide screens */
margin: 0 auto; /* Centers the navbar horizontally within the full-width header */
padding: 1rem 1.5rem; /* Breathing room around the logo and links */
}

Step 2: Arrange the Desktop Navbar with Flexbox

With the shell in place, Flexbox is what turns the logo and the link list into a single horizontal row. `justify-content: space-between` is doing the heavy lifting here — it pushes the logo to the far left and the link list to the far right, with any leftover space distributed between them.

.navbar {
/* ...existing rules from Step 1 stay... */
display: flex; /* Turns .navbar into a flex container, laying its children in a row */
align-items: center; /* Vertically centers the logo against the (taller) link list */
justify-content: space-between; /* Logo pinned left, nav menu pinned right, gap in between */
}
.navbar__logo {
font-size: 1.25rem;
font-weight: 700;
color: #ffffff;
text-decoration: none; /* Anchor tags are underlined by default; the logo shouldn't look like a link */
}
.navbar__links {
display: flex; /* Lays every <li> out horizontally instead of the default stacked list */
align-items: center; /* Keeps all the links vertically aligned with each other */
list-style: none; /* Removes the bullet point every <li> gets by default */
gap: 1.75rem; /* Even spacing between links, without needing margin on each individual <li> */
}

Step 3: Style the Nav Links and CTA Button

The plain links need a hover state so the navbar feels interactive, and the last link ("Sign Up") is styled as a distinct call-to-action button rather than a plain text link, so it draws the eye as the primary action in the header.

.navbar__link {
color: #d1d5db; /* Light gray — readable, but visually secondary to the white logo */
text-decoration: none;
font-size: 0.95rem;
font-weight: 500;
transition: color 0.2s ease; /* Animates the color change on hover instead of an instant snap */
}
.navbar__link:hover {
color: #ffffff; /* Brightens to full white on hover, signaling that the link is interactive */
}
.navbar__link--cta {
background: #6366f1; /* Accent color makes this link visually distinct from the plain-text links */
color: #ffffff;
padding: 0.5rem 1rem;
border-radius: 6px; /* Softly rounded corners give it a button-like shape */
}
.navbar__link--cta:hover {
background: #4f46e5; /* Slightly darker accent on hover, a different hover treatment than plain links */
color: #ffffff; /* Reasserted so it doesn't fall back to the plain .navbar__link hover color */
}

Step 4: Build the Hamburger Icon with the Checkbox Hack

This is the core CSS-only trick of the project. The `<input type="checkbox">` from the markup is completely hidden with `display: none` — but hiding it does not disable it, it can still be toggled by clicking its `<label>`, and CSS can still read its `:checked` state. The label itself is styled to look like three horizontal bars, which is the universal hamburger icon shape.

/* The checkbox itself is never meant to be visible — only its checked STATE matters to the CSS below */
.nav-toggle {
display: none;
}
.nav-toggle-label {
display: none; /* Hidden for now — Step 5's media query reveals it only on small screens */
flex-direction: column; /* Stacks the three bar <span>s vertically inside the label */
justify-content: space-between;
width: 24px;
height: 18px;
cursor: pointer; /* Shows a pointer cursor so it visually reads as clickable, like a button */
}
.nav-toggle-label span {
display: block;
height: 2px; /* Each bar is a thin horizontal line */
width: 100%;
background: #ffffff; /* White bars stand out against the dark header background */
border-radius: 2px;
transition: transform 0.25s ease, opacity 0.25s ease; /* Animates the open-state "X" morph added below */
}
/* Sibling selector: when the checkbox is :checked, style the <label>'s spans that follow it (~).
This only works because the checkbox and its label are siblings in the HTML markup. */
.nav-toggle:checked ~ .nav-toggle-label span:nth-child(1) {
transform: translateY(8px) rotate(45deg); /* Top bar rotates down into one diagonal stroke of an X */
}
.nav-toggle:checked ~ .nav-toggle-label span:nth-child(2) {
opacity: 0; /* Middle bar disappears, leaving just the two diagonal strokes that form the X */
}
.nav-toggle:checked ~ .nav-toggle-label span:nth-child(3) {
transform: translateY(-8px) rotate(-45deg); /* Bottom bar rotates up into the other diagonal stroke */
}

The `~` in `.nav-toggle:checked ~ .nav-toggle-label` is the general sibling combinator — it selects `.nav-toggle-label` only when it appears somewhere after `.nav-toggle:checked` under the same parent. That single relationship is the entire mechanism behind the checkbox hack: no JavaScript event listener is needed because CSS itself reacts to the checkbox's built-in checked/unchecked state.

Step 5: Reveal the Mobile Menu at Small Widths

Now the pieces come together inside a media query. Below 768px, the hamburger label is shown, the desktop link row is turned into an absolutely positioned dropdown, and the same `:checked ~` sibling trick from Step 4 is reused — this time to expand the dropdown's height from `0` to a value tall enough to show every link.

@media (max-width: 768px) {
.nav-toggle-label {
display: flex; /* Reveal the hamburger icon now that we're below tablet width */
}
.navbar__menu {
position: absolute; /* Takes the menu out of normal flow so it overlays the page below the header */
top: 100%; /* Positioned directly under the header */
left: 0;
right: 0;
background: #111827; /* Matches the header background so the dropdown feels attached to it */
overflow: hidden; /* Clips the link list while max-height keeps it collapsed */
max-height: 0; /* Collapsed by default — this IS the "closed" state of the menu */
transition: max-height 0.3s ease; /* Animates opening/closing instead of an instant jump */
}
.navbar__links {
flex-direction: column; /* Stack links vertically inside the dropdown instead of a horizontal row */
align-items: stretch;
gap: 0;
padding: 0.5rem 1.5rem;
}
.navbar__link {
padding: 0.75rem 0; /* Larger, easier-to-tap targets for touchscreens */
border-bottom: 1px solid rgba(255, 255, 255, 0.08); /* Subtle divider between each stacked link */
}
/* Reusing the checkbox-hack sibling selector: when checked, expand the (currently 0px) dropdown */
.nav-toggle:checked ~ .navbar__menu {
max-height: 300px; /* Generously tall enough to fit every link, revealing the whole dropdown */
}
}

Step 6: Make the Header Sticky and Polish

The last step is a small usability upgrade — pinning the header to the top of the viewport with `position: sticky` so the navbar (and its menu toggle) stay reachable no matter how far the user scrolls — plus a little spacing for the page content underneath.

.site-header {
position: sticky; /* Sticks to the top of its scroll container instead of scrolling away with the page */
top: 0; /* Pins exactly at the top edge of the viewport */
z-index: 100; /* Keeps the header (and its open dropdown) layered above the page content below it */
}
.page-content {
max-width: 1100px; /* Matches .navbar's max-width so page content and header stay visually aligned */
margin: 0 auto;
padding: 3rem 1.5rem;
}
.page-content h1 {
margin-bottom: 0.75rem;
font-size: 1.75rem;
}

Complete Code

Here is the fixed HTML markup again for reference, followed by the complete stylesheet with every rule from Steps 1 through 6 assembled in order.

<header class="site-header">
<div class="navbar">
<a href="#" class="navbar__logo">DevSite</a>
<input type="checkbox" id="nav-toggle" class="nav-toggle">
<label for="nav-toggle" class="nav-toggle-label">
<span></span>
<span></span>
<span></span>
</label>
<nav class="navbar__menu">
<ul class="navbar__links">
<li><a href="#" class="navbar__link">Home</a></li>
<li><a href="#" class="navbar__link">Courses</a></li>
<li><a href="#" class="navbar__link">Projects</a></li>
<li><a href="#" class="navbar__link">About</a></li>
<li><a href="#" class="navbar__link navbar__link--cta">Sign Up</a></li>
</ul>
</nav>
</div>
</header>
<main class="page-content">
<h1>Welcome to DevSite</h1>
<p>Shrink the browser (or this preview) below tablet width to see the navbar collapse into a hamburger menu.</p>
</main>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
color: #1f2937;
background: #f8fafc;
line-height: 1.5;
}
.site-header {
background: #111827;
color: #ffffff;
position: sticky;
top: 0;
z-index: 100;
}
.navbar {
max-width: 1100px;
margin: 0 auto;
padding: 1rem 1.5rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.navbar__logo {
font-size: 1.25rem;
font-weight: 700;
color: #ffffff;
text-decoration: none;
}
.navbar__links {
display: flex;
align-items: center;
list-style: none;
gap: 1.75rem;
}
.navbar__link {
color: #d1d5db;
text-decoration: none;
font-size: 0.95rem;
font-weight: 500;
transition: color 0.2s ease;
}
.navbar__link:hover {
color: #ffffff;
}
.navbar__link--cta {
background: #6366f1;
color: #ffffff;
padding: 0.5rem 1rem;
border-radius: 6px;
}
.navbar__link--cta:hover {
background: #4f46e5;
color: #ffffff;
}
.nav-toggle {
display: none;
}
.nav-toggle-label {
display: none;
flex-direction: column;
justify-content: space-between;
width: 24px;
height: 18px;
cursor: pointer;
}
.nav-toggle-label span {
display: block;
height: 2px;
width: 100%;
background: #ffffff;
border-radius: 2px;
transition: transform 0.25s ease, opacity 0.25s ease;
}
.nav-toggle:checked ~ .nav-toggle-label span:nth-child(1) {
transform: translateY(8px) rotate(45deg);
}
.nav-toggle:checked ~ .nav-toggle-label span:nth-child(2) {
opacity: 0;
}
.nav-toggle:checked ~ .nav-toggle-label span:nth-child(3) {
transform: translateY(-8px) rotate(-45deg);
}
.page-content {
max-width: 1100px;
margin: 0 auto;
padding: 3rem 1.5rem;
}
.page-content h1 {
margin-bottom: 0.75rem;
font-size: 1.75rem;
}
@media (max-width: 768px) {
.nav-toggle-label {
display: flex;
}
.navbar__menu {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #111827;
overflow: hidden;
max-height: 0;
transition: max-height 0.3s ease;
}
.navbar__links {
flex-direction: column;
align-items: stretch;
gap: 0;
padding: 0.5rem 1.5rem;
}
.navbar__link {
padding: 0.75rem 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.nav-toggle:checked ~ .navbar__menu {
max-height: 300px;
}
}
Live Preview

Sample Run

On a wide screen, the preview above shows the logo on the left and the horizontal link row (with the highlighted "Sign Up" button) on the right, sitting inside a dark sticky header. Narrow the browser window — or shrink this page — below 768px and the link row disappears, replaced by the three-bar hamburger icon on the right. Click it and the checkbox underneath toggles to `checked`, which expands `.navbar__menu`'s `max-height` and animates the dropdown open while the hamburger bars morph into an "X". Click again to close it — all without a single line of JavaScript.

Extend This Project

  • Add a `prefers-color-scheme` media query so the navbar switches to a light theme automatically on light-mode systems.
  • Make the logo swap to a smaller size on mobile using a media query, to leave more room for the hamburger icon.
  • Add a dropdown submenu to one of the nav links using a nested `<ul>` and a `:hover`/focus-based reveal.
  • Animate the hamburger icon's bars with a slightly longer, staggered `transition-delay` on each `<span>` for a more polished open/close feel.
  • Add an `aria-label` to the `<label>` and test the menu with keyboard-only navigation (Tab and Enter) to make the checkbox hack accessible.

Summary

You built a fully responsive navigation bar that lays itself out with Flexbox on desktop and collapses into a hamburger-triggered dropdown on mobile — entirely in CSS. The checkbox-hack pattern you used here (a hidden checkbox, a label styled as a control, and a `:checked ~` sibling selector) is a reusable trick you will see again anywhere a small site needs simple, JavaScript-free interactivity: toggles, accordions, and tabs can all be built the same way.