Navigation Patterns

Build responsive navigation components for headers, sidebars, and breadcrumbs.

Overview#

Navigation is crucial for user experience. This pattern covers:

  • Header navigation with mobile menu
  • Sidebar navigation with active states
  • Breadcrumb trails
  • Tab navigation
  • Command palette

Prerequisites#

npm install lucide-react cmdk

Header Navigation#

A responsive header with logo, navigation links, and user menu.

1// components/layout/Header.tsx 2import Link from 'next/link' 3import { auth } from '@/auth' 4 5const navItems = [ 6 { href: '/features', label: 'Features' }, 7 { href: '/pricing', label: 'Pricing' }, 8 { href: '/docs', label: 'Docs' } 9] 10 11export async function Header() { 12 const session = await auth() 13 14 return ( 15 <header className="sticky top-0 z-50 border-b bg-white"> 16 <nav className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4"> 17 <div className="flex items-center gap-8"> 18 <Link href="/" className="text-xl font-bold"> 19 Logo 20 </Link> 21 22 <ul className="hidden gap-6 md:flex"> 23 {navItems.map(item => ( 24 <li key={item.href}> 25 <Link 26 href={item.href} 27 className="text-gray-600 hover:text-gray-900" 28 > 29 {item.label} 30 </Link> 31 </li> 32 ))} 33 </ul> 34 </div> 35 36 <div className="flex items-center gap-4"> 37 {session ? ( 38 <UserMenu user={session.user} /> 39 ) : ( 40 <> 41 <Link href="/login">Sign in</Link> 42 <Link 43 href="/signup" 44 className="rounded-lg bg-black px-4 py-2 text-white" 45 > 46 Get Started 47 </Link> 48 </> 49 )} 50 </div> 51 </nav> 52 </header> 53 ) 54}

Mobile Navigation#

A slide-out mobile menu.

1// components/layout/MobileNav.tsx 2'use client' 3 4import { useState } from 'react' 5import Link from 'next/link' 6import { Menu, X } from 'lucide-react' 7 8interface NavItem { 9 href: string 10 label: string 11} 12 13export function MobileNav({ items }: { items: NavItem[] }) { 14 const [isOpen, setIsOpen] = useState(false) 15 16 return ( 17 <div className="md:hidden"> 18 <button 19 onClick={() => setIsOpen(!isOpen)} 20 className="p-2" 21 aria-label="Toggle menu" 22 > 23 {isOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />} 24 </button> 25 26 {isOpen && ( 27 <div className="absolute left-0 right-0 top-16 border-b bg-white p-4"> 28 <ul className="space-y-4"> 29 {items.map(item => ( 30 <li key={item.href}> 31 <Link 32 href={item.href} 33 onClick={() => setIsOpen(false)} 34 className="block py-2" 35 > 36 {item.label} 37 </Link> 38 </li> 39 ))} 40 </ul> 41 </div> 42 )} 43 </div> 44 ) 45}

A sidebar with icons and active state tracking.

1// components/layout/Sidebar.tsx 2'use client' 3 4import Link from 'next/link' 5import { usePathname } from 'next/navigation' 6import { Home, Users, Settings, BarChart, Folder, Bell } from 'lucide-react' 7import { cn } from '@/lib/utils' 8 9const sidebarItems = [ 10 { href: '/dashboard', label: 'Dashboard', icon: Home }, 11 { href: '/dashboard/projects', label: 'Projects', icon: Folder }, 12 { href: '/dashboard/users', label: 'Users', icon: Users }, 13 { href: '/dashboard/analytics', label: 'Analytics', icon: BarChart }, 14 { href: '/dashboard/notifications', label: 'Notifications', icon: Bell }, 15 { href: '/dashboard/settings', label: 'Settings', icon: Settings } 16] 17 18export function Sidebar() { 19 const pathname = usePathname() 20 21 return ( 22 <aside className="w-64 border-r bg-gray-50"> 23 <nav className="p-4"> 24 <ul className="space-y-1"> 25 {sidebarItems.map(item => { 26 const isActive = pathname === item.href || 27 (item.href !== '/dashboard' && pathname.startsWith(item.href)) 28 29 return ( 30 <li key={item.href}> 31 <Link 32 href={item.href} 33 className={cn( 34 'flex items-center gap-3 rounded-lg px-3 py-2', 35 isActive 36 ? 'bg-gray-200 text-gray-900' 37 : 'text-gray-600 hover:bg-gray-100' 38 )} 39 > 40 <item.icon className="h-5 w-5" /> 41 {item.label} 42 </Link> 43 </li> 44 ) 45 })} 46 </ul> 47 </nav> 48 </aside> 49 ) 50}

Show the current location within the site hierarchy.

1// components/ui/Breadcrumbs.tsx 2import Link from 'next/link' 3import { ChevronRight, Home } from 'lucide-react' 4 5interface BreadcrumbItem { 6 label: string 7 href?: string 8} 9 10export function Breadcrumbs({ items }: { items: BreadcrumbItem[] }) { 11 return ( 12 <nav aria-label="Breadcrumb" className="flex items-center gap-2 text-sm"> 13 <Link href="/" className="text-gray-500 hover:text-gray-700"> 14 <Home className="h-4 w-4" /> 15 </Link> 16 17 {items.map((item, index) => ( 18 <div key={index} className="flex items-center gap-2"> 19 <ChevronRight className="h-4 w-4 text-gray-400" /> 20 {item.href ? ( 21 <Link 22 href={item.href} 23 className="text-gray-500 hover:text-gray-700" 24 > 25 {item.label} 26 </Link> 27 ) : ( 28 <span className="text-gray-900">{item.label}</span> 29 )} 30 </div> 31 ))} 32 </nav> 33 ) 34} 35 36// Auto-generate from pathname 37'use client' 38 39import { usePathname } from 'next/navigation' 40 41export function useBreadcrumbs() { 42 const pathname = usePathname() 43 44 const items: BreadcrumbItem[] = pathname 45 .split('/') 46 .filter(Boolean) 47 .map((segment, index, arr) => ({ 48 label: segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, ' '), 49 href: index === arr.length - 1 ? undefined : '/' + arr.slice(0, index + 1).join('/') 50 })) 51 52 return items 53}

Command Palette#

A keyboard-driven command palette for quick navigation.

1// components/CommandPalette.tsx 2'use client' 3 4import { useState, useEffect } from 'react' 5import { Command } from 'cmdk' 6import { useRouter } from 'next/navigation' 7import { Search, Home, Settings, User, FileText } from 'lucide-react' 8 9export function CommandPalette() { 10 const [open, setOpen] = useState(false) 11 const router = useRouter() 12 13 useEffect(() => { 14 function handleKeyDown(e: KeyboardEvent) { 15 if (e.key === 'k' && (e.metaKey || e.ctrlKey)) { 16 e.preventDefault() 17 setOpen(o => !o) 18 } 19 } 20 21 document.addEventListener('keydown', handleKeyDown) 22 return () => document.removeEventListener('keydown', handleKeyDown) 23 }, []) 24 25 const navigate = (href: string) => { 26 router.push(href) 27 setOpen(false) 28 } 29 30 return ( 31 <Command.Dialog open={open} onOpenChange={setOpen} className="fixed inset-0 z-50"> 32 <div className="fixed inset-0 bg-black/50" /> 33 <div className="fixed left-1/2 top-1/4 w-full max-w-lg -translate-x-1/2 rounded-lg bg-white shadow-xl"> 34 <div className="flex items-center border-b px-4"> 35 <Search className="h-4 w-4 text-gray-400" /> 36 <Command.Input 37 placeholder="Search..." 38 className="w-full border-0 p-4 outline-none" 39 /> 40 </div> 41 42 <Command.List className="max-h-80 overflow-auto p-2"> 43 <Command.Empty className="p-4 text-center text-gray-500"> 44 No results found. 45 </Command.Empty> 46 47 <Command.Group heading="Navigation"> 48 <Command.Item onSelect={() => navigate('/dashboard')}> 49 <Home className="mr-2 h-4 w-4" /> 50 Dashboard 51 </Command.Item> 52 <Command.Item onSelect={() => navigate('/settings')}> 53 <Settings className="mr-2 h-4 w-4" /> 54 Settings 55 </Command.Item> 56 <Command.Item onSelect={() => navigate('/profile')}> 57 <User className="mr-2 h-4 w-4" /> 58 Profile 59 </Command.Item> 60 </Command.Group> 61 62 <Command.Group heading="Recent"> 63 <Command.Item onSelect={() => navigate('/docs/getting-started')}> 64 <FileText className="mr-2 h-4 w-4" /> 65 Getting Started Guide 66 </Command.Item> 67 </Command.Group> 68 </Command.List> 69 70 <div className="border-t p-2 text-center text-xs text-gray-500"> 71 Press <kbd className="rounded bg-gray-100 px-1">Esc</kbd> to close 72 </div> 73 </div> 74 </Command.Dialog> 75 ) 76}

Best Practices#

  1. Highlight active states - Users should always know where they are
  2. Use consistent navigation - Keep navigation placement predictable
  3. Support keyboard navigation - Tab, Enter, and arrow keys should work
  4. Make mobile navigation accessible - Hamburger menus should be discoverable
  5. Add skip links - Allow keyboard users to skip to main content