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 cmdkHeader 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}Sidebar Navigation#
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}Breadcrumbs#
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#
- Highlight active states - Users should always know where they are
- Use consistent navigation - Keep navigation placement predictable
- Support keyboard navigation - Tab, Enter, and arrow keys should work
- Make mobile navigation accessible - Hamburger menus should be discoverable
- Add skip links - Allow keyboard users to skip to main content