| name | navigation-patterns |
| description | Navigation design patterns. Sidebars, top navs, tabs, breadcrumbs, mobile navigation, routing. |
Navigation Patterns
Good navigation is invisible. Bad navigation is all users talk about.
1. Navigation Types
| Pattern | Items | Use When |
|---|
| Top nav | 3–7 | Marketing sites, simple apps |
| Left sidebar | 5–20 | Complex apps, dashboards |
| Bottom tab bar | 3–5 | Mobile apps |
| Tabs | 2–7 | Section switching, same context |
| Breadcrumbs | Dynamic | Deep hierarchy, content sites |
| Hamburger menu | Any | Mobile, collapsed nav |
| Mega menu | Many | E-commerce, large sites |
2. Sidebar Navigation
const navigation = [
{ name: "Dashboard", href: "/dashboard", icon: HomeIcon },
{
name: "Projects",
icon: FolderIcon,
children: [
{ name: "Active", href: "/projects/active" },
{ name: "Archived", href: "/projects/archived" },
],
},
{ name: "Settings", href: "/settings", icon: CogIcon },
];
function Sidebar() {
const pathname = usePathname();
return (
<nav className="w-64 bg-gray-900 min-h-screen p-4">
{navigation.map(item => (
item.children ? (
<CollapsibleNavItem key={item.name} item={item} currentPath={pathname} />
) : (
<NavLink
key={item.name}
href={item.href}
active={pathname.startsWith(item.href)}
>
<item.icon className="w-5 h-5" />
{item.name}
</NavLink>
)
))}
</nav>
);
}
3. Active State Patterns
function NavLink({ href, active, children }: NavLinkProps) {
return (
<Link
href={href}
className={cn(
"flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors",
active
? "bg-blue-600 text-white"
: "text-gray-400 hover:text-white hover:bg-gray-800"
)}
aria-current={active ? "page" : undefined}
>
{children}
</Link>
);
}
4. Breadcrumbs
function Breadcrumbs({ items }: { items: { label: string; href?: string }[] }) {
return (
<nav aria-label="Breadcrumb">
<ol className="flex items-center gap-1 text-sm text-gray-500">
{items.map((item, i) => (
<li key={i} className="flex items-center gap-1">
{i > 0 && <ChevronRight className="w-3 h-3" />}
{item.href && i < items.length - 1 ? (
<Link href={item.href} className="hover:text-gray-900">
{item.label}
</Link>
) : (
<span className="text-gray-900 font-medium" aria-current="page">
{item.label}
</span>
)}
</li>
))}
</ol>
</nav>
);
}
function useBreadcrumbs() {
const pathname = usePathname();
segments = pathname.().();
segments.( ({
: (segment.(, )),
: i < segments. - ? + segments.(, i + ).() : ,
}));
}
5. Mobile Bottom Tab Bar
const tabs = [
{ name: "Home", href: "/", icon: HomeIcon },
{ name: "Search", href: "/search", icon: SearchIcon },
{ name: "Notifications", href: "/notifications", icon: BellIcon, badge: 3 },
{ name: "Profile", href: "/profile", icon: UserIcon },
];
function BottomTabBar() {
const pathname = usePathname();
return (
<nav className="fixed bottom-0 left-0 right-0 bg-white border-t safe-area-bottom">
<div className="flex">
{tabs.map(tab => (
<Link
key={tab.name}
href={tab.href}
className={cn(
"flex-1 flex flex-col items-center py-2 gap-1",
=== ? "" ""
)}
>
{tab.badge && (
{tab.badge}
)}
{tab.name}
))}
);
}
6. Responsive Navigation
function AppNav() {
const [mobileOpen, setMobileOpen] = useState(false);
return (
<>
{/* Desktop sidebar */}
<aside className="hidden lg:block w-64">
<Sidebar />
</aside>
{/* Mobile trigger */}
<button
className="lg:hidden"
onClick={() => setMobileOpen(true)}
aria-label="Open menu"
>
<Menu className="w-6 h-6" />
</button>
{/* Mobile drawer */}
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
<SheetContent side="left" className="p-0 w-64">
<Sidebar onNavigate={() => setMobileOpen(false)} />
</SheetContent>
</Sheet>
</>
);
}
7. Anti-Patterns
| ❌ Don't | ✅ Do |
|---|
| No active state | Clearly mark current page |
| Hamburger menu on desktop | Visible nav on desktop |
| Deep nesting (4+ levels) | Flatten or redesign IA |
| No keyboard navigation | Tab + Enter + Escape support |
| Missing aria-current | Accessibility attribute on active |
| Nested navigation in navigation | One primary nav |