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 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>
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; } }
display: grid (or flex) also lets you perfectly center the three bars inside the button with place-items: center.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); }
transform, not right or left. Transforms are GPU-accelerated and stay smooth at 60fps; animating position properties forces slow layout recalculations on every frame.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.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; }
z-index (drawer higher, overlay lower, header highest) so the menu stays on top while the rest of the page dims behind it.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); }
Every way to close it
Good menus close intuitively. Wire up four closing actions so users never feel trapped:
- Tapping the hamburger again (it's now an X) — already handled by the toggle.
- Tapping the overlay backdrop.
- Tapping any link inside the drawer.
- Pressing the Esc key.
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(); });
Make it accessible
- Use a real
<button>for the hamburger — it's keyboard-focusable and works with Enter/Space for free. - Give it an
aria-label(“Open menu”) since the icon has no text, and togglearia-expandedtrue/false. - Wrap the links in a
<nav>so assistive tech announces the navigation landmark. - Closing on Esc (Step 8) is a keyboard-user expectation.
- Respect
prefers-reduced-motion— disable the slide for users who get motion sickness.
@media (prefers-reduced-motion: reduce) { .drawer, .overlay, .bars span { transition: none; } }
<button> instead of a clickable <div>. It gives you focus, keyboard activation, and screen-reader semantics with zero extra code.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
- Hamburger is a real
<button>with anaria-label - Burger shows only below your breakpoint; inline links show above it
- Drawer parked off-screen with
translateX(100%), animated via a.openclass - Animate
transform(notleft/right) for smoothness - Closes on overlay tap, link tap, and the Esc key
-
aria-expandedtoggled;prefers-reduced-motionrespected