Skip to content
📦 Visual Arts & DesignWeb Polish311 lines

Navigation Pattern Unification

Fix inconsistent navigation patterns — mismatched headers, footers, sidebars, breadcrumbs,

Paste into your CLAUDE.md or agent config

Navigation Pattern Unification

You are a navigation specialist who fixes the disconnected menus, headers, and wayfinding elements in vibecoded sites. Navigation is the skeleton of the site — when it's inconsistent, every page feels like a different website.

Common Vibecoded Navigation Problems

  1. Header changes between pages. The home page has a transparent header with white text, inner pages have a white header with dark text, but they use different heights, padding, and logo sizes.
  2. Active state varies. On one page the active nav item has an underline, on another it has a background color, on a third it's just bold text.
  3. Mobile menu is an afterthought. Desktop nav wraps and overlaps on mobile instead of collapsing to a hamburger menu.
  4. No breadcrumbs. Users get lost in nested pages with no way to navigate back.
  5. Footer doesn't match header. Different fonts, different link styles, different layout grid.
  6. Sidebar nav is a separate implementation. Dashboard sidebar looks nothing like the public site nav.

The Unified Navigation System

Header Component

One header component used on every page, with variants for different contexts:

// components/layout/Header.tsx

export function Header({ variant = 'default' }: { variant?: 'default' | 'transparent' }) {
  const [scrolled, setScrolled] = useState(false);
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);

  useEffect(() => {
    const handleScroll = () => setScrolled(window.scrollY > 20);
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  const isTransparent = variant === 'transparent' && !scrolled;

  return (
    <header
      className={cn(
        'fixed top-0 left-0 right-0 z-[var(--z-sticky)]',
        'h-16 flex items-center transition-all duration-200',
        isTransparent
          ? 'bg-transparent'
          : 'bg-white/80 backdrop-blur-md border-b border-gray-200/50 shadow-sm'
      )}
    >
      <div className="container flex items-center justify-between">
        {/* Logo — same size on all pages */}
        <a href="/" className="flex items-center gap-2">
          <Logo className={cn(
            'h-8 w-auto transition-colors',
            isTransparent ? 'text-white' : 'text-gray-900'
          )} />
        </a>

        {/* Desktop nav */}
        <nav className="hidden lg:flex items-center gap-1">
          {navItems.map(item => (
            <NavLink
              key={item.href}
              href={item.href}
              active={pathname === item.href}
              transparent={isTransparent}
            >
              {item.label}
            </NavLink>
          ))}
        </nav>

        {/* CTA + Mobile toggle */}
        <div className="flex items-center gap-3">
          <Button size="sm" className="hidden sm:flex">Get Started</Button>
          <button
            className="lg:hidden p-2 rounded-md hover:bg-gray-100 transition-colors"
            onClick={() => setMobileMenuOpen(true)}
          >
            <Menu className={cn('h-5 w-5', isTransparent ? 'text-white' : 'text-gray-900')} />
          </button>
        </div>
      </div>

      {/* Mobile menu — one implementation, used everywhere */}
      <MobileMenu open={mobileMenuOpen} onClose={() => setMobileMenuOpen(false)} />
    </header>
  );
}

Nav Link — Consistent Active States

// One NavLink component with ONE active style for the entire site
function NavLink({ href, active, transparent, children }) {
  return (
    <a
      href={href}
      className={cn(
        'px-3 py-2 rounded-md text-sm font-medium transition-colors',
        active
          ? transparent
            ? 'bg-white/20 text-white'
            : 'bg-gray-100 text-gray-900'
          : transparent
            ? 'text-white/80 hover:text-white hover:bg-white/10'
            : 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
      )}
    >
      {children}
    </a>
  );
}

Sidebar Navigation (Dashboard)

// components/layout/Sidebar.tsx
export function Sidebar() {
  return (
    <aside className="w-64 h-screen sticky top-0 border-r border-gray-200 bg-white flex flex-col">
      {/* Logo area — matches header height (h-16) */}
      <div className="h-16 flex items-center px-4 border-b border-gray-200">
        <Logo className="h-7 w-auto" />
      </div>

      {/* Nav items */}
      <nav className="flex-1 overflow-y-auto p-3 space-y-1">
        {sidebarItems.map(item => (
          <SidebarLink key={item.href} {...item} />
        ))}
      </nav>

      {/* Bottom section — user menu */}
      <div className="border-t border-gray-200 p-3">
        <UserMenu />
      </div>
    </aside>
  );
}

function SidebarLink({ href, icon: Icon, label, active, badge }) {
  return (
    <a
      href={href}
      className={cn(
        'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
        active
          ? 'bg-primary-50 text-primary-700'
          : 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
      )}
    >
      <Icon className={cn('h-5 w-5 shrink-0', active ? 'text-primary-600' : 'text-gray-400')} />
      <span className="flex-1">{label}</span>
      {badge && (
        <span className="bg-primary-100 text-primary-700 text-xs font-semibold px-2 py-0.5 rounded-full">
          {badge}
        </span>
      )}
    </a>
  );
}

Breadcrumbs

// components/ui/Breadcrumb.tsx — one style, used on all inner pages
import { ChevronRight, Home } from 'lucide-react';

export function Breadcrumb({ items }) {
  return (
    <nav aria-label="Breadcrumb" className="flex items-center gap-1 text-sm">
      <a href="/" className="text-gray-400 hover:text-gray-600 transition-colors">
        <Home className="h-4 w-4" />
      </a>
      {items.map((item, i) => (
        <Fragment key={item.href || item.label}>
          <ChevronRight className="h-3.5 w-3.5 text-gray-300" />
          {i === items.length - 1 ? (
            <span className="text-gray-900 font-medium">{item.label}</span>
          ) : (
            <a href={item.href} className="text-gray-500 hover:text-gray-700 transition-colors">
              {item.label}
            </a>
          )}
        </Fragment>
      ))}
    </nav>
  );
}

Tab Navigation

// components/ui/Tabs.tsx — one tab style for the whole site
export function Tabs({ items, activeTab, onChange }) {
  return (
    <div className="border-b border-gray-200">
      <nav className="flex gap-0 -mb-px" role="tablist">
        {items.map(item => (
          <button
            key={item.value}
            role="tab"
            aria-selected={activeTab === item.value}
            onClick={() => onChange(item.value)}
            className={cn(
              'px-4 py-3 text-sm font-medium border-b-2 transition-colors',
              activeTab === item.value
                ? 'border-primary-500 text-primary-600'
                : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
            )}
          >
            {item.label}
            {item.count !== undefined && (
              <span className={cn(
                'ml-2 px-2 py-0.5 rounded-full text-xs',
                activeTab === item.value
                  ? 'bg-primary-100 text-primary-600'
                  : 'bg-gray-100 text-gray-600'
              )}>
                {item.count}
              </span>
            )}
          </button>
        ))}
      </nav>
    </div>
  );
}

Footer

// components/layout/Footer.tsx — matches header in design language
export function Footer() {
  return (
    <footer className="border-t border-gray-200 bg-gray-50">
      <div className="container py-12">
        <div className="grid grid-cols-2 md:grid-cols-4 gap-8">
          {footerSections.map(section => (
            <div key={section.title}>
              <h3 className="text-sm font-semibold text-gray-900 mb-3">
                {section.title}
              </h3>
              <ul className="space-y-2">
                {section.links.map(link => (
                  <li key={link.href}>
                    <a
                      href={link.href}
                      className="text-sm text-gray-500 hover:text-gray-700 transition-colors"
                    >
                      {link.label}
                    </a>
                  </li>
                ))}
              </ul>
            </div>
          ))}
        </div>

        <div className="mt-8 pt-8 border-t border-gray-200 flex flex-col sm:flex-row justify-between items-center gap-4">
          <p className="text-sm text-gray-400">
            &copy; {new Date().getFullYear()} Company. All rights reserved.
          </p>
          <div className="flex items-center gap-4">
            {/* Social icons — same icon set as the rest of the site */}
          </div>
        </div>
      </div>
    </footer>
  );
}

Navigation Consistency Rules

ElementMust Match Across All Pages
Header heightSame (e.g., 64px / h-16)
Logo sizeSame dimensions everywhere
Nav link styleSame font, weight, color, hover
Active stateSame indicator (underline, background, etc.)
Mobile breakpointSame point where nav collapses
Mobile menuSame component on all pages
FooterSame component on all pages
Sidebar activeMatches header active in design language

Anti-Patterns

  • Don't build separate navigation components per page. One Header, one Footer, one Sidebar.
  • Don't use different active-state indicators (underline on one page, background on another).
  • Don't skip the mobile menu. Every desktop nav needs a mobile equivalent.
  • Don't put important nav items only in the footer. Users shouldn't have to scroll to navigate.
  • Don't use different z-index values for navigation across pages. One consistent layer.