Professional navigation requires visual feedback. Users need to know where they are in your application and when navigation is happening. Active link highlighting shows the current page, loading indicators show navigation progress, and hover states provide interactive feedback. Let's build navigation components that feel polished and professional with proper active states, loading indicators, and smooth transitions!
Basic Active Link Pattern
Simple Active Link Component
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
interface NavLinkProps {
href: string;
children: React.ReactNode;
}
export function NavLink({ href, children }: NavLinkProps) {
const pathname = usePathname();
const isActive = pathname === href;
return (
<Link
href={href}
className={
isActive
? 'text-blue-600 font-semibold border-b-2 border-blue-600'
: 'text-gray-700 hover:text-blue-600 transition'
}
>
{children}
</Link>
);
}
// Usage:
// <NavLink href="/about">About</NavLink>
// <NavLink href="/blog">Blog</NavLink>
// ✅ Compares current pathname with href
// ✅ Different styles for active vs inactive
// ✅ Hover effect on inactive linksActive Link with Exact vs Partial Match
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
interface NavLinkProps {
href: string;
children: React.ReactNode;
exact?: boolean; // Default false
}
export function NavLink({ href, children, exact = false }: NavLinkProps) {
const pathname = usePathname();
// Exact match: pathname must equal href exactly
// Partial match: pathname starts with href (for nested routes)
const isActive = exact
? pathname === href
: pathname === href || pathname.startsWith(href + '/');
return (
<Link
href={href}
className={`px-4 py-2 rounded-lg transition ${
isActive
? 'bg-blue-600 text-white font-semibold'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
{children}
</Link>
);
}
// Usage:
// <NavLink href="/" exact>Home</NavLink> // Only active on "/"
// <NavLink href="/blog">Blog</NavLink> // Active on "/blog/*"
// <NavLink href="/about" exact>About</NavLink> // Only active on "/about"
// Examples:
// URL: / → Home is active
// URL: /blog → Blog is active
// URL: /blog/post-1 → Blog is STILL active (partial match)
// URL: /about → About is active
// ✅ Exact match for home and specific pages
// ✅ Partial match for sections with nested routes
// ✅ Flexible active state logicAdvanced Active Link Styling
Multiple Style Variants
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
type Variant = 'default' | 'pill' | 'underline' | 'sidebar';
interface NavLinkProps {
href: string;
children: React.ReactNode;
variant?: Variant;
exact?: boolean;
}
export function NavLink({
href,
children,
variant = 'default',
exact = false
}: NavLinkProps) {
const pathname = usePathname();
const isActive = exact
? pathname === href
: pathname === href || pathname.startsWith(href + '/');
const styles = {
default: isActive
? 'text-blue-600 font-semibold'
: 'text-gray-700 hover:text-blue-600',
pill: isActive
? 'bg-blue-600 text-white px-4 py-2 rounded-full font-semibold'
: 'text-gray-700 hover:bg-gray-100 px-4 py-2 rounded-full',
underline: isActive
? 'text-blue-600 font-semibold border-b-2 border-blue-600 pb-1'
: 'text-gray-700 hover:text-blue-600 hover:border-b-2 hover:border-gray-300 pb-1',
sidebar: isActive
? 'bg-blue-50 text-blue-600 font-semibold border-l-4 border-blue-600 px-4 py-2'
: 'text-gray-700 hover:bg-gray-50 hover:border-l-4 hover:border-gray-300 px-4 py-2',
};
return (
<Link href={href} className={`${styles[variant]} transition`}>
{children}
</Link>
);
}
// Usage:
// <NavLink href="/blog" variant="pill">Blog</NavLink>
// <NavLink href="/about" variant="underline">About</NavLink>
// <NavLink href="/docs" variant="sidebar">Docs</NavLink>
// ✅ Multiple design patterns
// ✅ Consistent active states
// ✅ Smooth transitions
// ✅ Reusable componentActive Link with Icon
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
interface NavLinkProps {
href: string;
icon: React.ReactNode;
children: React.ReactNode;
exact?: boolean;
}
export function NavLinkWithIcon({
href,
icon,
children,
exact = false
}: NavLinkProps) {
const pathname = usePathname();
const isActive = exact
? pathname === href
: pathname === href || pathname.startsWith(href + '/');
return (
<Link
href={href}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition ${
isActive
? 'bg-blue-600 text-white font-semibold'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
<span className={`text-xl ${isActive ? 'opacity-100' : 'opacity-60'}`}>
{icon}
</span>
<span>{children}</span>
{isActive && (
<span className="ml-auto">✓</span>
)}
</Link>
);
}
// Usage:
// <NavLinkWithIcon href="/dashboard" icon={<DashboardIcon />}>
// Dashboard
// </NavLinkWithIcon>
// <NavLinkWithIcon href="/settings" icon={<SettingsIcon />}>
// Settings
// </NavLinkWithIcon>
// ✅ Icon included in component
// ✅ Icon opacity changes with active state
// ✅ Checkmark indicator when active
// ✅ Professional sidebar navigationNested Navigation with Active States
Sidebar with Nested Links
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useState } from 'react';
interface NavItem {
href: string;
label: string;
children?: NavItem[];
}
const navigation: NavItem[] = [
{ href: '/dashboard', label: 'Dashboard' },
{
href: '/products',
label: 'Products',
children: [
{ href: '/products/list', label: 'All Products' },
{ href: '/products/new', label: 'Add Product' },
{ href: '/products/categories', label: 'Categories' },
],
},
{
href: '/orders',
label: 'Orders',
children: [
{ href: '/orders/pending', label: 'Pending' },
{ href: '/orders/completed', label: 'Completed' },
],
},
{ href: '/settings', label: 'Settings' },
];
function NavItem({ item }: { item: NavItem }) {
const pathname = usePathname();
const [isOpen, setIsOpen] = useState(
pathname.startsWith(item.href)
);
const isActive = pathname === item.href || pathname.startsWith(item.href + '/');
const isExactMatch = pathname === item.href;
return (
<div>
{/* Parent Link */}
<div className="flex items-center">
<Link
href={item.href}
className={`flex-1 flex items-center gap-3 px-4 py-3 rounded-lg transition ${
isExactMatch
? 'bg-blue-600 text-white font-semibold'
: isActive
? 'bg-blue-50 text-blue-600 font-semibold'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
{item.label}
</Link>
{/* Expand/Collapse Button */}
{item.children && (
<button
onClick={() => setIsOpen(!isOpen)}
className="p-2 hover:bg-gray-100 rounded"
>
<span className={`transition-transform ${isOpen ? 'rotate-180' : ''}`}>
▼
</span>
</button>
)}
</div>
{/* Nested Links */}
{item.children && isOpen && (
<div className="ml-6 mt-1 space-y-1">
{item.children.map(child => {
const isChildActive = pathname === child.href;
return (
<Link
key={child.href}
href={child.href}
className={`block px-4 py-2 rounded-lg transition ${
isChildActive
? 'bg-blue-600 text-white font-semibold'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
{child.label}
</Link>
);
})}
</div>
)}
</div>
);
}
export function Sidebar() {
return (
<aside className="w-64 h-screen bg-white border-r p-4">
<div className="space-y-2">
{navigation.map(item => (
<NavItem key={item.href} item={item} />
))}
</div>
</aside>
);
}
// ✅ Nested navigation with expand/collapse
// ✅ Parent link highlighted when on child route
// ✅ Auto-expands when on nested route
// ✅ Smooth transitions
// ✅ Professional sidebar navigationLoading States During Navigation
Navigation Loading Indicator with useTransition
'use client';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { useTransition } from 'react';
interface NavLinkProps {
href: string;
children: React.ReactNode;
}
export function NavLinkWithLoading({ href, children }: NavLinkProps) {
const pathname = usePathname();
const router = useRouter();
const [isPending, startTransition] = useTransition();
const isActive = pathname === href;
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
startTransition(() => {
router.push(href);
});
};
return (
<Link
href={href}
onClick={handleClick}
className={`relative px-4 py-2 rounded-lg transition ${
isActive
? 'bg-blue-600 text-white font-semibold'
: 'text-gray-700 hover:bg-gray-100'
} ${isPending ? 'opacity-50 cursor-wait' : ''}`}
>
{children}
{isPending && (
<span className="absolute right-2 top-1/2 -translate-y-1/2">
<span className="inline-block w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
</span>
)}
</Link>
);
}
// ✅ Shows spinner during navigation
// ✅ Dims link while loading
// ✅ Cursor changes to wait
// ✅ Better user feedbackGlobal Navigation Loading Bar
'use client';
import { useEffect, useState } from 'react';
import { usePathname } from 'next/navigation';
export function LoadingBar() {
const pathname = usePathname();
const [loading, setLoading] = useState(false);
useEffect(() => {
// Show loading bar
setLoading(true);
// Hide after short delay (simulates loading)
const timer = setTimeout(() => {
setLoading(false);
}, 300);
return () => clearTimeout(timer);
}, [pathname]);
if (!loading) return null;
return (
<div className="fixed top-0 left-0 right-0 h-1 bg-blue-600 z-50 animate-pulse">
<div className="h-full bg-blue-400 animate-[loading_1s_ease-in-out_infinite]" />
</div>
);
}
// Add to layout.tsx:
// export default function RootLayout({ children }) {
// return (
// <html>
// <body>
// <LoadingBar />
// {children}
// </body>
// </html>
// );
// }
// ✅ Shows at top of page during navigation
// ✅ Automatic - no configuration needed
// ✅ Smooth animation
// ✅ Non-intrusive visual feedbackLoading State with Progress
'use client';
import { useEffect, useState } from 'react';
import { usePathname } from 'next/navigation';
export function ProgressBar() {
const pathname = usePathname();
const [progress, setProgress] = useState(0);
const [visible, setVisible] = useState(false);
useEffect(() => {
// Reset and show
setProgress(0);
setVisible(true);
// Animate progress
const interval = setInterval(() => {
setProgress(prev => {
if (prev >= 90) {
clearInterval(interval);
return prev;
}
return prev + 10;
});
}, 100);
// Complete after route change
const timer = setTimeout(() => {
setProgress(100);
setTimeout(() => setVisible(false), 200);
}, 500);
return () => {
clearInterval(interval);
clearTimeout(timer);
};
}, [pathname]);
if (!visible) return null;
return (
<div className="fixed top-0 left-0 right-0 h-1 bg-gray-200 z-50">
<div
className="h-full bg-blue-600 transition-all duration-200"
style={{ width: `${progress}%` }}
/>
</div>
);
}
// ✅ Realistic progress animation
// ✅ Completes when navigation finishes
// ✅ Smooth transitions
// ✅ Professional loading feedbackComplete Navigation Examples
Example 1: Complete Navbar with Active Links
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
const links = [
{ href: '/', label: 'Home', exact: true },
{ href: '/about', label: 'About', exact: true },
{ href: '/blog', label: 'Blog', exact: false },
{ href: '/products', label: 'Products', exact: false },
{ href: '/contact', label: 'Contact', exact: true },
];
export function Navbar() {
const pathname = usePathname();
return (
<nav className="bg-white shadow-md">
<div className="container mx-auto px-4">
<div className="flex items-center justify-between h-16">
{/* Logo */}
<Link href="/" className="text-2xl font-bold text-blue-600">
MyApp
</Link>
{/* Navigation Links */}
<ul className="flex gap-1">
{links.map(link => {
const isActive = link.exact
? pathname === link.href
: pathname === link.href || pathname.startsWith(link.href + '/');
return (
<li key={link.href}>
<Link
href={link.href}
className={`px-4 py-2 rounded-lg transition font-medium ${
isActive
? 'bg-blue-600 text-white'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
{link.label}
</Link>
</li>
);
})}
</ul>
{/* CTA Button */}
<Link
href="/signup"
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition font-semibold"
>
Sign Up
</Link>
</div>
</div>
</nav>
);
}
// ✅ Clean, professional navbar
// ✅ Active link highlighting
// ✅ Exact match for specific pages
// ✅ Partial match for sections
// ✅ Hover effects
// ✅ Responsive design readyExample 2: Tabs with Active State
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
interface Tab {
href: string;
label: string;
count?: number;
}
interface TabsProps {
tabs: Tab[];
basePath: string;
}
export function Tabs({ tabs, basePath }: TabsProps) {
const pathname = usePathname();
return (
<div className="border-b border-gray-200">
<nav className="flex gap-8">
{tabs.map(tab => {
const isActive = pathname === tab.href;
return (
<Link
key={tab.href}
href={tab.href}
className={`pb-4 px-1 border-b-2 transition ${
isActive
? 'border-blue-600 text-blue-600 font-semibold'
: 'border-transparent text-gray-600 hover:text-gray-900 hover:border-gray-300'
}`}
>
<span>{tab.label}</span>
{tab.count !== undefined && (
<span
className={`ml-2 px-2 py-1 text-xs rounded-full ${
isActive
? 'bg-blue-100 text-blue-600'
: 'bg-gray-100 text-gray-600'
}`}
>
{tab.count}
</span>
)}
</Link>
);
})}
</nav>
</div>
);
}
// Usage:
// <Tabs
// basePath="/dashboard"
// tabs={[
// { href: '/dashboard/overview', label: 'Overview' },
// { href: '/dashboard/analytics', label: 'Analytics', count: 12 },
// { href: '/dashboard/reports', label: 'Reports', count: 3 },
// ]}
// />
// ✅ Tab-style navigation
// ✅ Active border indicator
// ✅ Optional count badges
// ✅ Hover effects
// ✅ Clean, modern designExample 3: Mobile Menu with Active States
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useState } from 'react';
const links = [
{ href: '/', label: 'Home' },
{ href: '/about', label: 'About' },
{ href: '/blog', label: 'Blog' },
{ href: '/products', label: 'Products' },
{ href: '/contact', label: 'Contact' },
];
export function MobileMenu() {
const pathname = usePathname();
const [isOpen, setIsOpen] = useState(false);
return (
<div className="lg:hidden">
{/* Hamburger Button */}
<button
onClick={() => setIsOpen(!isOpen)}
className="p-2 hover:bg-gray-100 rounded-lg"
aria-label="Toggle menu"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{isOpen ? (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
) : (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
)}
</svg>
</button>
{/* Mobile Menu Overlay */}
{isOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/50 z-40"
onClick={() => setIsOpen(false)}
/>
{/* Menu */}
<div className="fixed top-0 right-0 bottom-0 w-64 bg-white shadow-xl z-50 p-6">
<button
onClick={() => setIsOpen(false)}
className="absolute top-4 right-4 p-2 hover:bg-gray-100 rounded-lg"
>
✕
</button>
<nav className="mt-12 space-y-2">
{links.map(link => {
const isActive =
pathname === link.href ||
(link.href !== '/' && pathname.startsWith(link.href));
return (
<Link
key={link.href}
href={link.href}
onClick={() => setIsOpen(false)}
className={`block px-4 py-3 rounded-lg transition ${
isActive
? 'bg-blue-600 text-white font-semibold'
: 'text-gray-700 hover:bg-gray-100'
}`}
>
{link.label}
</Link>
);
})}
</nav>
</div>
</>
)}
</div>
);
}
// ✅ Mobile-friendly navigation
// ✅ Slide-in menu
// ✅ Active link highlighting
// ✅ Backdrop overlay
// ✅ Close on link click
// ✅ Accessible (aria-label)Active Links Project Structure
Organized navigation components with active states
Select a file or folder to see details
Best Practices
1. Use Exact Match Wisely
// ✅ GOOD: Exact match for home
<NavLink href="/" exact>Home</NavLink>
// ✅ GOOD: Partial match for sections
<NavLink href="/blog">Blog</NavLink>
// Active on /blog, /blog/post-1, /blog/tech, etc.
// ❌ BAD: Partial match for home
<NavLink href="/">Home</NavLink>
// Would be active on EVERY page!2. Provide Visual Feedback
// ✅ GOOD: Multiple visual cues
const activeStyles = 'bg-blue-600 text-white font-semibold border-l-4 border-blue-800';
const inactiveStyles = 'text-gray-700 hover:bg-gray-100';
// Color + font weight + border = clear active state
// ❌ BAD: Subtle difference only
const activeStyles = 'text-blue-600';
const inactiveStyles = 'text-gray-600';
// Too subtle - hard to see which is active3. Show Loading States
// ✅ GOOD: Loading indicator during navigation
{isPending && <Spinner />}
// ✅ GOOD: Global loading bar
<LoadingBar />
// Provides feedback that navigation is happening4. Make Touch Targets Large Enough
// ✅ GOOD: Large enough for mobile
<Link className="px-4 py-3"> {/* 44px+ height */}
Link Text
</Link>
// ❌ BAD: Too small
<Link className="px-2 py-1"> {/* ~20px height */}
Link Text
</Link>5. Accessibility Considerations
// ✅ GOOD: Indicate current page for screen readers
<Link
href="/about"
aria-current={isActive ? 'page' : undefined}
className={isActive ? activeStyles : inactiveStyles}
>
About
</Link>
// ✅ GOOD: Descriptive labels
<button aria-label="Open navigation menu">
☰
</button>Key Takeaways
- usePathname() - compare with href for active state
- Exact vs partial match - exact for specific pages, partial for sections
- Visual feedback - clear difference between active/inactive
- Loading indicators - use useTransition() for navigation state
- Nested navigation - highlight parent when on child route
- Multiple variants - pills, underlines, sidebars
- Mobile-friendly - large touch targets, clear states
- Accessibility - aria-current for screen readers
What's Next?
You've mastered active links and navigation states! Next, we'll explore Redirects and Navigation Guards—how to protect routes, implement authentication checks, redirect users based on conditions, and build secure navigation flows.
You'll learn to control access to routes, redirect unauthenticated users, implement role-based access, and create robust authentication flows.
🎨 Design Consistency
Keep your active link styling consistent across your entire application. Pick one style (pills, underlines, backgrounds) and use it everywhere. Consistency helps users quickly understand where they are in your application!