← Back to the Mobile Nav tutorial
Lesson

Mobile Navigation, Step by Step

Phones don't have room for a full menu bar, so we hide the links behind a hamburger button on the right that slides a navigation drawer in from the right edge. This lesson builds exactly that — one piece at a time — ending in a working demo you can open and close.

Step 1

The hamburger pattern

A desktop navbar shows every link in a row. A phone is only ~375px wide — there's no room. The near-universal solution: collapse the links behind a hamburger button (the three-line icon), placed in the top corner. Tapping it slides out a navigation drawer.

We'll put the hamburger on the right and slide the drawer in from the right — comfortable for right-handed thumb reach, and the layout this lesson builds throughout.

The whole feature is three moving parts: a button (the hamburger), a panel (the drawer), and a bit of JavaScript that toggles a class to open and close it. Everything else is styling and polish.
Step 2

The HTML structure

Start with a header: the logo on the left, the hamburger button on the right. Then the drawer (a <nav>) and an overlay, both as siblings. Semantic <button> and <nav> matter for accessibility.

<header class="top">
  <a class="logo" href="#">My Site</a>

  <!-- hamburger button, on the right -->
  <button class="burger" id="burger"
          aria-label="Open menu" aria-expanded="false">
    <span class="bars"><span></span><span></span><span></span></span>
  </button>
</header>

<!-- the slide-in drawer + dim backdrop -->
<nav class="drawer" id="drawer">
  <a href="#">Home</a>
  <a href="#">About</a>
  <a href="#">Contact</a>
</nav>
<div class="overlay" id="overlay"></div>
My Site
logo left · hamburger right — the header at rest
Step 3

Show the burger only on small screens

On wide screens you usually show the links inline and hide the hamburger; on phones you hide the links and show the hamburger. A media query flips between the two. The header uses justify-content: space-between so the logo and button sit at opposite ends.

.top { display: flex; justify-content: space-between; align-items: center; }

/* mobile-first: burger shows, inline links hidden */
.burger      { display: grid; }
.desktop-links { display: none; }

@media (min-width: 768px) {   desktop */
  .burger      { display: none; }
  .desktop-links { display: flex; }
}
Putting the burger display: grid (or flex) also lets you perfectly center the three bars inside the button with place-items: center.
Step 4

Park the drawer off-screen, on the right

The drawer is pinned to the right edge with position: fixed, then pushed completely off-screen with transform: translateX(100%) (100% of its own width to the right). A transition on transform makes it glide when we bring it back.

.drawer {
  position: fixed;
  top: 0; right: 0; bottom: 0;   pin to the right edge, full height
  width: 75%; max-width: 320px;
  transform: translateX(100%);   hidden, just past the right edge
  transition: transform .3s ease;
}

/* the .open class (added by JS) slides it into view */
.drawer.open { transform: translateX(0); }
Animate transform, not right or left. Transforms are GPU-accelerated and stay smooth at 60fps; animating position properties forces slow layout recalculations on every frame.
Step 5

The toggle: a few lines of JavaScript

All the JS does is add or remove the .open class when the button is clicked — CSS handles the actual animation. We also keep aria-expanded in sync so screen readers announce the state.

const burger  = document.getElementById('burger');
const drawer  = document.getElementById('drawer');
const overlay = document.getElementById('overlay');

function toggleMenu() {
  const open = drawer.classList.toggle('open');
  overlay.classList.toggle('open', open);
  burger.classList.toggle('open', open);
  burger.setAttribute('aria-expanded', open);
  burger.setAttribute('aria-label', open ? 'Close menu' : 'Open menu');
}

burger.addEventListener('click', toggleMenu);
classList.toggle('open') returns true when it just added the class — we reuse that boolean to keep the overlay, burger animation, and ARIA all in lockstep.
Step 6

The dim backdrop

An overlay behind the drawer dims the page, focuses attention, and gives users a big tap target to close the menu. It fades with opacity, and pointer-events makes it ignore clicks until it's visible.

.overlay {
  position: fixed; inset: 0;
  background: rgba(0,0,0,.45);
  opacity: 0; pointer-events: none;   invisible & un-clickable when closed
  transition: opacity .3s;
}
.overlay.open { opacity: 1; pointer-events: auto; }
Sit the overlay below the drawer with z-index (drawer higher, overlay lower, header highest) so the menu stays on top while the rest of the page dims behind it.
Step 7

Morph the hamburger into an X

A nice touch: when the menu is open, animate the three bars into a close (X) icon. Rotate the top and bottom bars to cross, and fade the middle one out — pure CSS, triggered by the .open class.

.bars span { transition: transform .3s, opacity .2s; }

.burger.open .bars span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.burger.open .bars span:nth-child(2) { opacity: 0; }
.burger.open .bars span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
The top bar drops to the middle and tilts 45°; the bottom rises to meet it at −45°; the middle fades away — and you've got a crisp X. Try it in the live demo below.
Step 8

Every way to close it

Good menus close intuitively. Wire up four closing actions so users never feel trapped:

function closeMenu() {
  drawer.classList.remove('open');
  overlay.classList.remove('open');
  burger.classList.remove('open');
  burger.setAttribute('aria-expanded', 'false');
}

overlay.addEventListener('click', closeMenu);
drawer.querySelectorAll('a').forEach(a => a.addEventListener('click', closeMenu));
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeMenu(); });
Closing on link-click matters most: on a one-page site the drawer would otherwise stay open covering the section the user just jumped to.
Step 9

Make it accessible

@media (prefers-reduced-motion: reduce) {
  .drawer, .overlay, .bars span { transition: none; }
}
The biggest win: using <button> instead of a clickable <div>. It gives you focus, keyboard activation, and screen-reader semantics with zero extra code.
Step 10

The finished thing — try it

Everything together. Tap the hamburger on the right — the drawer slides in from the right, the page dims, and the bars morph into an X. Close it by tapping the X, the dimmed area, or any link:

Welcome

This is the page content. The menu lives behind the hamburger in the top-right corner.

Tap the icon to slide the navigation in from the right.

Tip: tap the hamburger, then close it with the X, the dim backdrop, or a link.

Mobile nav checklist

Where next

Keep going