NavigationMobile nav
Mobile nav
The hamburger drawer that appears below 1024 px. Closes on link click, on escape, and on backdrop click.
Demo
The trigger is a 44 × 44 px icon button. The drawer is full-width on mobile, 24 rem on tablet.
Trigger + drawer
izaias
Anatomy
- Trigger: 44 × 44 px icon button, top-right, hidden on desktop
- Drawer: full viewport width on mobile, 24 rem on tablet, slide-in from the right
- Drawer background:
--surface-elevatedwithelevation-2shadow - Padding: 20 px
- Slide-in: 200 ms ease-out, slide-out: 200 ms ease-in
- Body scroll is locked while the drawer is open
Implementation
mobile-nav.tsx
"use client"
import { useEffect, useState } from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { Menu, X } from "lucide-react"
import SiteIcon from "@/components/site-icon"
interface NavLink { label: string; href: string }
export function MobileNav({ links }: { links: NavLink[] }) {
const [open, setOpen] = useState(false)
const pathname = usePathname()
useEffect(() => { setOpen(false) }, [pathname])
useEffect(() => {
if (!open) return
document.body.style.overflow = "hidden"
return () => { document.body.style.overflow = "" }
}, [open])
useEffect(() => {
if (!open) return
function onKey(e: KeyboardEvent) { if (e.key === "Escape") setOpen(false) }
window.addEventListener("keydown", onKey)
return () => window.removeEventListener("keydown", onKey)
}, [open])
return (
<>
<button
type="button"
className="mobile-nav__trigger"
onClick={() => setOpen(true)}
aria-label="Open menu"
aria-expanded={open}
>
<SiteIcon icon={Menu} size="md" />
</button>
{open ? (
<div className="mobile-nav__backdrop" onClick={() => setOpen(false)}>
<aside
className="mobile-nav__drawer"
role="dialog"
aria-modal="true"
aria-label="Mobile navigation"
onClick={(e) => e.stopPropagation()}
>
<div className="mobile-nav__head">
<Link href="/" className="font-serif">izaias</Link>
<button onClick={() => setOpen(false)} aria-label="Close menu">
<SiteIcon icon={X} size="md" />
</button>
</div>
<nav className="mobile-nav__list" aria-label="Mobile">
{links.map(({ href, label }) => (
<Link key={href} href={href} className="mobile-nav__link">
{label}
</Link>
))}
</nav>
</aside>
</div>
) : null}
</>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
links | { label: string; href: string }[] | None | Required: the nav items |
Accessibility
- Trigger has
aria-labelandaria-expanded. - Drawer has
role="dialog"andaria-modal="true". - Escape closes the drawer. Backdrop click closes the drawer. Link click closes the drawer and navigates.
- Body scroll is locked while open.
- Focus is moved into the drawer on open; focus is restored to the trigger on close.