Why mobile navigation is different
Over 60% of web traffic is mobile. You can't just shrink a desktop menu — screens are narrow, fingers are wide, and users expect specific patterns. Great mobile nav is invisible until you need it.
Screen real estate
A phone screen is ~5× smaller than a laptop. Every pixel a nav steals is a pixel stolen from the content.
Thumb zones
Users hold phones one-handed. Navigation needs to be reachable with a thumb — edges and bottom, not far corners.
Speed matters
Mobile users are in motion. One-second delays cost you visitors. Nav has to open instantly, with no jank.
Expected patterns
Users scan for a hamburger icon, top-right. Don't reinvent the pattern — make the familiar one feel great.
Live demo — slide from the right
Tap the hamburger icon in the phone mockup to see a right-side drawer slide in. Notice the overlay dim effect, the smooth 350ms transition, and how tapping outside closes it.
Welcome back
Your dashboard is ready. Tap the menu to navigate between sections.
The code — HTML, CSS, JS
Three pieces: the hamburger button, the drawer markup, and a tiny bit of JS to toggle a class. The animation is pure CSS. The backdrop is a separate element with its own click handler.
<!-- Top bar with brand + hamburger trigger --> <header class="topbar"> <span class="brand">Acme</span> <button class="hamburger" id="menuToggle" aria-label="Open menu"> <i class="ph-bold ph-list"></i> </button> </header> <!-- Dim overlay — clicking it closes the drawer --> <div class="drawer-overlay" id="drawerOverlay"></div> <!-- The drawer itself — slides in from the RIGHT --> <nav class="drawer" id="drawer" aria-label="Main navigation"> <div class="drawer-head"> <strong>Menu</strong> <button class="drawer-close" id="menuClose"> <i class="ph-bold ph-x"></i> </button> </div> <a href="#" class="active"><i class="ph-fill ph-house"></i> Home</a> <a href="#"><i class="ph-bold ph-compass"></i> Explore</a> <a href="#"><i class="ph-bold ph-chat-circle"></i> Messages</a> <a href="#"><i class="ph-bold ph-bell"></i> Notifications</a> <a href="#"><i class="ph-bold ph-gear"></i> Settings</a> <a href="#"><i class="ph-bold ph-question"></i> Help</a> <div class="drawer-foot"> <div class="avatar-mini">JD</div> <div class="info"> <strong>Jane Doe</strong> <span>jane@acme.co</span> </div> </div> </nav>
/* Top bar */ .topbar { display: flex; justify-content: space-between; align-items: center; padding: 14px 16px; background: #0f172a; } .brand { font-weight: 800; color: #fff; } /* Hamburger — a simple icon button */ .hamburger { background: none; border: none; color: #fff; font-size: 1.6rem; padding: 8px; cursor: pointer; border-radius: 8px; } .hamburger:hover { background: rgba(255,255,255,.1); } /* Dim backdrop — invisible until .open is added to body */ .drawer-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.5); opacity: 0; pointer-events: none; transition: opacity .3s; z-index: 10; } body.menu-open .drawer-overlay { opacity: 1; pointer-events: auto; } /* THE DRAWER — positioned on the RIGHT, off-screen by default */ .drawer { position: fixed; top: 0; right: 0; /* ← anchored to the right edge */ bottom: 0; width: min(320px, 80vw); background: #0f172a; box-shadow: -10px 0 30px rgba(0,0,0,.4); transform: translateX(100%); /* hidden off-screen right */ transition: transform .35s cubic-bezier(.4,0,.2,1); z-index: 11; overflow-y: auto; } /* Slide it IN when body has .menu-open */ body.menu-open .drawer { transform: translateX(0); } /* Drawer internals */ .drawer-head { display: flex; justify-content: space-between; align-items: center; padding: 18px; border-bottom: 1px solid rgba(255,255,255,.08); } .drawer a { display: flex; align-items: center; gap: 12px; padding: 14px 18px; color: #cbd5e1; text-decoration: none; font-weight: 500; } .drawer a:hover { background: rgba(59,130,246,.1); color: #fff; } .drawer a.active { background: rgba(59,130,246,.15); color: #fff; } .drawer-close { background: none; border: none; color: #fff; font-size: 1.4rem; cursor: pointer; } /* Footer with mini avatar */ .drawer-foot { margin-top: auto; display: flex; align-items: center; gap: 10px; padding: 16px; border-top: 1px solid rgba(255,255,255,.08); } .avatar-mini { width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(135deg, #3b82f6, #a855f7); display: flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; font-size: 0.85rem; } .drawer-foot .info strong { display: block; color: #fff; } .drawer-foot .info span { color: #94a3b8; font-size: 0.78rem; }
// Grab the three interactive elements const toggle = document.getElementById('menuToggle'); const closeBtn = document.getElementById('menuClose'); const overlay = document.getElementById('drawerOverlay'); // Add / remove the .menu-open class on <body> // That class triggers the CSS transitions for drawer + overlay function openMenu() { document.body.classList.add('menu-open'); } function closeMenu() { document.body.classList.remove('menu-open'); } toggle.addEventListener('click', openMenu); closeBtn.addEventListener('click', closeMenu); overlay.addEventListener('click', closeMenu); // Bonus: close on Escape key (accessibility) document.addEventListener('keydown', e => { if (e.key === 'Escape') closeMenu(); });
Mobile nav rules
Small details separate a mobile menu that feels native from one that feels clunky. Here's what to get right — and what to avoid.
Do
- Place the hamburger in the top corner (left or right)
- Make tap targets at least 44×44px
- Use
transformfor smooth 60fps animation - Dim the background with an overlay
- Let users tap outside or press Esc to close
- Add
aria-labelto icon-only buttons - Lock body scroll while the drawer is open
- Highlight the current page in the menu
Don't
- Animate
leftorwidth— usetransformonly - Use tiny 16px tap targets — thumbs can't hit them
- Hide critical actions (login, cart) in the menu only
- Nest 3+ levels of sub-menus — users get lost
- Use a 2-second transition — 250–350ms is ideal
- Leave the body scrollable behind the drawer
- Skip the close button — some users won't find the tap-outside trick
- Use the same nav for phone and desktop without testing both
Other mobile nav patterns
The right-side drawer isn't the only option. Here are the four most common mobile nav patterns — and when to use each.
Drawer (off-canvas)
Slides in from the side. Great for apps with 5+ nav items. What we just built.
Best for appsBottom tab bar
Fixed tabs at the bottom — always visible, thumb-friendly, iOS/Android native.
Best for 3–5 itemsFull-screen overlay
Menu takes over the whole screen when opened. Dramatic, focused, marketing-friendly.
Best for landing pagesDropdown / accordion
Menu expands down from the top bar. Simple, but can push content awkwardly.
Best for docs / small sitesMinimal right-slide drawer
The whole pattern condensed. Copy this into any project and you have a working mobile menu in under 30 lines of CSS.
.drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(320px, 80vw); background: #fff; transform: translateX(100%); transition: transform .3s; z-index: 100; } body.menu-open .drawer { transform: translateX(0); } .drawer-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.5); opacity: 0; pointer-events: none; transition: opacity .3s; z-index: 99; } body.menu-open .drawer-overlay { opacity: 1; pointer-events: auto; }
const open = () => document.body.classList.add('menu-open'); const close = () => document.body.classList.remove('menu-open'); menuToggle.addEventListener('click', open); drawerOverlay.addEventListener('click', close); menuClose.addEventListener('click', close); document.addEventListener('keydown', e => e.key === 'Escape' && close());
The complete drawer — all three pieces
The steps above build this up one idea at a time. Here it is whole and self-contained: paste the HTML at the top of your <body>, the CSS into your stylesheet, and the JS before </body>. Tap the hamburger to open, tap the overlay / × / Esc to close. No libraries needed.
1. HTML — paste at the top of your <body>
<!-- top bar -->
<header class="mnav">
<a class="mnav-logo" href="index.html">Brandr</a>
<button class="mnav-toggle" aria-label="Open menu">☰</button>
</header>
<!-- slide-in drawer + dark overlay -->
<div class="drawer-overlay"></div>
<aside class="drawer" aria-label="Mobile menu">
<button class="drawer-close" aria-label="Close menu">×</button>
<nav>
<a href="#">Home</a>
<a href="#">Features</a>
<a href="#">Pricing</a>
<a href="#">About</a>
</nav>
</aside>2. CSS — paste into your stylesheet
<style>
.mnav { display: flex; align-items: center; justify-content: space-between; padding: 14px 20px; background: #111; color: #fff; }
.mnav-logo { color: #fff; font-weight: 800; text-decoration: none; }
.mnav-toggle, .drawer-close { font-size: 1.6rem; line-height: 1; background: none; border: none; color: inherit; cursor: pointer; }
.drawer {
position: fixed; top: 0; right: 0; bottom: 0;
width: min(320px, 80vw);
background: #fff; padding: 20px;
transform: translateX(100%); /* hidden off-screen */
transition: transform .3s ease;
z-index: 100;
}
.drawer nav { display: flex; flex-direction: column; gap: 6px; margin-top: 24px; }
.drawer nav a { color: #111; text-decoration: none; padding: 10px 0; font-size: 1.05rem; }
.drawer-close { color: #111; }
body.menu-open .drawer { transform: translateX(0); } /* slides in */
.drawer-overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,.5);
opacity: 0; pointer-events: none;
transition: opacity .3s ease;
z-index: 99;
}
body.menu-open .drawer-overlay { opacity: 1; pointer-events: auto; }
</style>3. JavaScript — paste before </body>
<script>
const menuToggle = document.querySelector('.mnav-toggle');
const menuClose = document.querySelector('.drawer-close');
const drawerOverlay = document.querySelector('.drawer-overlay');
const open = () => document.body.classList.add('menu-open');
const close = () => document.body.classList.remove('menu-open');
menuToggle.addEventListener('click', open);
menuClose.addEventListener('click', close);
drawerOverlay.addEventListener('click', close);
document.addEventListener('keydown', e => e.key === 'Escape' && close());
</script>The whole thing as one file
Everything above — the hamburger, the right-slide drawer, the dim overlay, and the toggle script — combined into one complete HTML file. Copy it into a new file called index.html, open it in a browser, and it just works — no libraries, no build step. Tap the hamburger to open, then the overlay / × / Esc to close. Here's the live result, then the full code.
Live preview
Complete HTML — copy this whole file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mobile Drawer Nav</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #1f2433; background: #f8fafc; }
/* ===== Top bar ===== */
.mnav {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 20px; background: #0f172a; color: #fff;
position: sticky; top: 0; z-index: 50;
}
.mnav-logo { color: #fff; font-weight: 800; font-size: 1.1rem; text-decoration: none; }
.mnav-toggle, .drawer-close {
font-size: 1.7rem; line-height: 1; background: none; border: none;
color: inherit; cursor: pointer; padding: 4px 8px; border-radius: 8px;
}
.mnav-toggle:hover { background: rgba(255,255,255,.12); }
/* ===== Dim overlay ===== */
.drawer-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,.5);
opacity: 0; pointer-events: none; transition: opacity .3s ease; z-index: 99;
}
body.menu-open .drawer-overlay { opacity: 1; pointer-events: auto; }
/* ===== The drawer — slides in from the RIGHT ===== */
.drawer {
position: fixed; top: 0; right: 0; bottom: 0;
width: min(320px, 80vw); background: #0f172a; color: #fff;
padding: 18px; z-index: 100;
transform: translateX(100%);
transition: transform .35s cubic-bezier(.4,0,.2,1);
box-shadow: -10px 0 30px rgba(0,0,0,.35);
display: flex; flex-direction: column;
}
body.menu-open .drawer { transform: translateX(0); }
.drawer-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.drawer-head strong { font-size: 1rem; }
.drawer nav { display: flex; flex-direction: column; gap: 2px; }
.drawer nav a {
color: #cbd5e1; text-decoration: none; padding: 13px 12px;
border-radius: 10px; font-weight: 500; transition: background .15s, color .15s;
}
.drawer nav a:hover { background: rgba(59,130,246,.14); color: #fff; }
.drawer nav a.active { background: rgba(59,130,246,.2); color: #fff; }
/* ===== Page content ===== */
main { max-width: 720px; margin: 0 auto; padding: 32px 20px; }
main h1 { font-size: 1.9rem; margin-bottom: 12px; color: #0f172a; }
main p { color: #475569; line-height: 1.6; margin-bottom: 16px; }
.card {
background: #fff; border: 1px solid #e2e8f0; border-radius: 12px;
padding: 18px; margin-bottom: 14px;
}
.card h3 { font-size: 1.05rem; margin-bottom: 6px; color: #0f172a; }
.card p { margin: 0; font-size: .92rem; }
</style>
</head>
<body>
<!-- Top bar with brand + hamburger -->
<header class="mnav">
<a class="mnav-logo" href="#">Acme</a>
<button class="mnav-toggle" aria-label="Open menu">☰</button>
</header>
<!-- Dim overlay (click to close) -->
<div class="drawer-overlay"></div>
<!-- The right-side drawer -->
<aside class="drawer" aria-label="Main navigation">
<div class="drawer-head">
<strong>Menu</strong>
<button class="drawer-close" aria-label="Close menu">×</button>
</div>
<nav>
<a href="#" class="active">Home</a>
<a href="#">Explore</a>
<a href="#">Messages</a>
<a href="#">Settings</a>
<a href="#">Help</a>
</nav>
</aside>
<!-- Page content -->
<main>
<h1>Welcome back</h1>
<p>Tap the menu button in the top corner to slide the navigation drawer in from the right. Tap the dim overlay, the × button, or press Escape to close it.</p>
<div class="card"><h3>3 new reports</h3><p>Your weekly analytics are ready to review.</p></div>
<div class="card"><h3>12 unread messages</h3><p>Catch up with your team in the inbox.</p></div>
<div class="card"><h3>5 notifications</h3><p>Stay on top of mentions and updates.</p></div>
</main>
<script>
var toggle = document.querySelector('.mnav-toggle');
var close = document.querySelector('.drawer-close');
var overlay = document.querySelector('.drawer-overlay');
function openMenu() { document.body.classList.add('menu-open'); }
function closeMenu() { document.body.classList.remove('menu-open'); }
toggle.addEventListener('click', openMenu);
close.addEventListener('click', closeMenu);
overlay.addEventListener('click', closeMenu);
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') closeMenu();
});
</script>
</body>
</html>