Navbar

A persistent top-of-page navigation bar with brand identity, primary links, search, notifications, and a user menu. Collapses to a hamburger drawer on narrow screens. Includes a collapsible sidebar variant.

Top navigation

Dashboard

Click nav links to change the active state

Dashboard

Click nav links to change the active state. Click the avatar to open the user menu. On narrow screens the hamburger reveals the drawer.

Sidebar navigation

A vertical sidebar suits apps with more than five top-level destinations or where a wide content area benefits from a narrow left rail. Supports collapse-to-icon mode — the brand logo remains visible and each item gets a title tooltip when collapsed.

S
Sitka

Dashboard

JR

← Collapse sidebar • Active: Dashboard

Anatomy

ZonePlacementContents
BrandLeftLogo mark + product name. Links to home. Never omit on desktop.
Primary navCenter / left of center3–7 top-level destinations. Active link uses accent-subtle background.
SearchCenter or rightInline expand on focus (desktop) or icon-only that opens a full-screen overlay (mobile).
UtilityRightNotifications bell with unread dot, New/Create CTA, theme toggle.
User menuFar rightAvatar trigger. Dropdown shows name, email, profile, settings, sign out.
HamburgerFar right (mobile only)Hides/shows the mobile drawer. Morphs to ✕ when open.

Responsive behavior

BreakpointPrimary navSearchActions
≥ 768px (md)Horizontal in barExpandable inline inputAll visible
< 768pxHidden — hamburger drawerIcon onlyAvatar + hamburger only

Implementation

Navbar.tsx
tsx
"use client";

import { useState } from "react";
import { Menu, X, ChevronDown } from "lucide-react";
import { cn } from "@/lib";

const NAV_LINKS = ["Dashboard", "Team", "Analytics", "Settings"];

export function Navbar() {
  const [active, setActive]         = useState(NAV_LINKS[0]);
  const [mobileOpen, setMobileOpen] = useState(false);
  const [userOpen, setUserOpen]     = useState(false);

  return (
    <nav className="border-b bg-surface">
      <div className="px-4 h-14 flex items-center gap-3">

        {/* Brand */}
        <a href="/" className="flex items-center gap-2 flex-shrink-0 mr-2">
          <div className="w-7 h-7 rounded-lg bg-accent" />
          <span className="text-[14px] font-semibold">Sitka</span>
        </a>

        {/* Desktop links */}
        <div className="hidden md:flex items-center gap-0.5 flex-1">
          {NAV_LINKS.map((label) => (
            <button
              key={label}
              onClick={() => setActive(label)}
              className={cn(
                "px-3 py-1.5 rounded-lg text-[13px] font-medium transition-colors",
                active === label
                  ? "bg-accent-subtle text-accent"
                  : "text-text-secondary hover:bg-surface-raised"
              )}
            >
              {label}
            </button>
          ))}
        </div>

        <div className="flex-1 md:flex-none" />

        {/* Right: actions + user menu */}
        <UserMenu open={userOpen} onToggle={() => setUserOpen(!userOpen)} />

        {/* Mobile hamburger */}
        <button
          onClick={() => setMobileOpen(!mobileOpen)}
          className="md:hidden w-8 h-8 flex items-center justify-center rounded-lg"
        >
          {mobileOpen ? <X className="w-4 h-4" /> : <Menu className="w-4 h-4" />}
        </button>
      </div>

      {/* Mobile drawer */}
      {mobileOpen && (
        <div className="md:hidden border-t px-3 pb-3 pt-2">
          {NAV_LINKS.map((label) => (
            <button
              key={label}
              onClick={() => { setActive(label); setMobileOpen(false); }}
              className={cn(
                "w-full flex items-center px-3 py-2.5 rounded-lg text-[13px] font-medium",
                active === label ? "bg-accent-subtle text-accent" : "text-text-secondary"
              )}
            >
              {label}
            </button>
          ))}
        </div>
      )}
    </nav>
  );
}

Design decisions

  • Top nav vs. sidebar. Use a top navbar for apps with ≤ 6 destinations and a wide primary content area (dashboards, marketing tools). Use a sidebar when items exceed 6, destinations have sub-items, or vertical real estate is abundant (desktop-first apps).
  • Active state subtlety. The active link uses accent-subtle background (not a strong fill) so the bar doesn't compete with primary page content. The accent color alone carries enough weight.
  • Search positioning. An inline search bar that expands on focus saves space while remaining discoverable. If search is a primary action (e.g., documentation sites), give it more permanent width.
  • User menu as a portal. Render the user dropdown via createPortal to document.body so it is never clipped by overflow:hidden ancestors. Position it via getBoundingClientRect on open — the same approach as Tooltip and SplitButton.
  • Mobile drawer, not modal. The mobile nav slides down below the bar (not over the page content) for a lighter feel. It doesn't require a backdrop or focus trap because it is not a blocking overlay — users can still scroll the page behind it.

Accessibility

  • Wrap the navbar in a <nav> element with aria-label="Primary navigation" so screen readers can landmark-navigate to it.
  • The active link should have aria-current="page". Never rely on visual style alone.
  • The hamburger button must have aria-expanded and aria-controls pointing to the mobile drawer id.
  • The user menu trigger needs aria-haspopup="menu" and aria-expanded. The dropdown has role="menu"; items have role="menuitem".
  • Keyboard: Tab moves through focusable items left-to-right. Escape closes any open dropdown and returns focus to its trigger.
  • Don't put the skip-to-content link inside the navbar — place it as the very first focusable element on the page, above the navbar.