Tutorial

Mobile Navigation from scratch

Learn to build a slide-in mobile menu that opens from the right — with a hamburger toggle, smooth animation, overlay backdrop, and clean transitions. Tap the demo to see it work.

Step 1

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.

Step 2

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.

9:41
Acme

Welcome back

Your dashboard is ready. Tap the menu to navigate between sections.

📊 3 new reports
✉️ 12 unread messages
🔔 5 notifications
Tap to open · drawer slides in from the right
Step 3

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="#"><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-gear"></i> Settings</a>
</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-close {
  background: none;
  border: none;
  color: #fff;
  font-size: 1.4rem;
  cursor: pointer;
}
// 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();
});
Step 4

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 transform for smooth 60fps animation
  • Dim the background with an overlay
  • Let users tap outside or press Esc to close
  • Add aria-label to icon-only buttons
  • Lock body scroll while the drawer is open
  • Highlight the current page in the menu

Don't

  • Animate left or width — use transform only
  • 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
Step 5

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 apps

Bottom tab bar

Fixed tabs at the bottom — always visible, thumb-friendly, iOS/Android native.

Best for 3–5 items

Full-screen overlay

Menu takes over the whole screen when opened. Dramatic, focused, marketing-friendly.

Best for landing pages

Dropdown / accordion

Menu expands down from the top bar. Simple, but can push content awkwardly.

Best for docs / small sites
Why slide from the right? Right-handed users hold their phones with the right thumb — the hamburger ends up on the right too. Opening the drawer where the thumb already is means the menu items appear closer to the thumb than if they slid in from the left. It's a small bit of UX polish that adds up.
Cheat Sheet

Minimal 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());