Loading…
Loading…
Primary navigation element, typically at the top of the page, containing links and actions.
The Navigation Bar (navbar, header, app bar) is the primary navigation element of a website or application. It sits at the top of the page, provides access to the site's main sections, and establishes brand identity. It's the first thing users see and the element they return to when they're lost.
A navbar must balance multiple demands: it needs to be compact enough to not consume excessive vertical space, yet provide clear access to all primary destinations. It needs to work on a 320px mobile screen and a 2560px desktop monitor. It needs to accommodate a logo, 3–12 navigation links, a search bar (maybe), user account controls, and possibly a notification indicator — all without feeling cramped.
The single biggest challenge with navbars is responsive behavior. Desktop navbars typically show all links horizontally. Mobile navbars must collapse those links into a hamburger menu, a bottom sheet, or a slide-out drawer. This isn't just a CSS problem — it's a UX decision that affects discoverability, information architecture, and accessibility.
When to use a Navigation Bar:
When NOT to use a Navigation Bar:
Test your navbar's typography with our Font Pair Tester and validate spacing with the Spacing Calculator.
| Variant | Description | Best For |
|---|---|---|
| Standard | Fixed height, logo left, links center or right, actions far right. | Most websites and web apps. |
| Centered | Logo centered, links split evenly on both sides. | Marketing sites, brand-focused designs. |
| Transparent | No background, text overlays hero image/video. Background appears on scroll. | Landing pages, portfolio sites. |
| Sticky | Fixed to the top of the viewport on scroll. Always accessible. | Content-heavy sites, dashboards. |
| Shrinking | Starts large (with hero), shrinks to compact on scroll. | Marketing sites with tall hero sections. |
| Double-row | Primary nav row + secondary row (breadcrumbs, sub-nav, or search). | Enterprise apps, complex sites with deep IA. |
| Bottom (mobile) | Navigation at the bottom of the screen. Thumb-accessible. | Mobile-first apps (Instagram, Twitter style). |
| Pattern | Description | Pros / Cons |
|---|---|---|
| Hamburger menu | Three-line icon that opens a full-screen or slide-out menu. | Universal pattern. But: lower discoverability — links are hidden. |
| Bottom sheet | Links appear in a bottom-aligned sheet on tap. | Thumb-friendly. Less common, may confuse. |
| Priority+ (overflow) | Shows as many links as fit, moves the rest to a "More" dropdown. | Progressive disclosure. Complex to implement well. |
| Tab bar | Fixed bottom bar with 3–5 icon+label items (mobile only). | High discoverability. Limited to 5 items. App-like feel. |
For sites with complex information architecture, the mega menu variant expands a full-width dropdown below the navbar. It can contain:
Use mega menus for 15+ navigation items organized into categories (e.g., e-commerce category pages).
| Property | Type | Default | Description |
|---|---|---|---|
variant | 'standard' | 'transparent' | 'sticky' | 'standard' | Visual behavior |
logo | ReactNode | — | Brand logo or wordmark |
links | { label: string; href: string; active?: boolean }[] | [] | Primary navigation items |
actions | ReactNode | — | Right-side actions (search, notifications, user menu) |
mobileBreakpoint | number | 768 | Viewport width at which to switch to mobile layout |
sticky | boolean | false | Whether the navbar sticks to the top on scroll |
height | number | 64 | Navbar height in pixels |
maxWidth | number | string | '1280px' | Max-width of the navbar content |
hamburgerLabel | string | "Menu" | Accessible label for the mobile menu toggle |
onNavigate | (href: string) => void | — | Custom navigation handler (for SPA routing) |
| Token Category | Token Example | Navbar Usage |
|---|---|---|
| Color – Background | --color-surface, --color-surface-elevated | Navbar background. Transparent variant: transparent → --color-surface on scroll. |
| Color – Text | --color-text, --color-text-muted | Link text (active vs. inactive) |
| Color – Active | --color-primary-600 | Active link indicator (underline, background, or text color) |
| Color – Hover | --color-surface-hover | Link hover background |
| Color – Border | --color-border-subtle | Bottom border (optional, separates navbar from content) |
| Shadow | --shadow-sm | Elevation on scroll (sticky variant) |
| Spacing | --space-4 (16px), --space-6 (24px) | Horizontal padding, gap between links. Use Spacing Calculator. |
| Typography | --font-size-sm, --font-weight-medium | Link text styling. Test pairings with Font Pair Tester. |
| Height | --navbar-height (64px) | Consistent height token for scroll offset calculations |
| Z-index | --z-navbar (40) | Stacks above page content, below overlays |
| Transition | --duration-normal (200ms) | Background color transition (transparent → solid on scroll) |
See our Design Tokens Complete Guide for a full token architecture reference.
| State | Visual Change | Behavior |
|---|---|---|
| Default | Full background, all links visible (desktop). Hamburger icon visible (mobile). | Standard navigation |
| Scrolled | Shadow appears (sticky variant). Transparent variant gains solid background. Optionally shrinks height. | scroll event listener with throttling |
| Mobile – Closed | Only logo and hamburger icon visible | Collapsed state |
| Mobile – Open | Overlay menu visible (full-screen, slide-out, or dropdown) | Focus trapped in menu. Body scroll locked. |
| Link – Default | Muted text color | Inactive navigation item |
| Link – Hover | Background tint or text color darkens | Visual feedback |
| Link – Active | Primary color text, underline indicator, or filled background | Current page/section indicator |
| Link – Focus | Visible focus ring around the link | Keyboard navigation. Must be visible per WCAG 2.4.7. |
For sticky navbars, a common pattern is to show/hide the navbar based on scroll direction:
/* Navbar slides up when scrolling down, reappears on scroll up */
.navbar {
position: sticky;
top: 0;
transition: transform 200ms ease;
}
.navbar--hidden {
transform: translateY(-100%);
}
This preserves vertical space while keeping navigation quickly accessible. Implement with IntersectionObserver or a scroll direction detector.
| Criterion | Level | Requirement |
|---|---|---|
| SC 1.3.1 Info and Relationships | A | Use the <nav> element with aria-label (e.g., aria-label="Main navigation"). If multiple <nav> elements exist, each must have a unique label. |
| SC 2.4.5 Multiple Ways | AA | Provide at least two ways to reach each page (e.g., navigation + search, or navigation + site map). |
| SC 2.4.8 Location | AAA | Indicate the user's current location within the navigation (active link styling + aria-current="page"). |
| SC 2.4.7 Focus Visible | AA | All navigation links must have visible focus indicators. |
| SC 2.5.8 Target Size | AA | Touch targets must be at least 24×24px. Navigation links should have generous padding. |
<header class="navbar" role="banner">
<nav aria-label="Main navigation">
<a href="/" aria-label="Stellae Design — Home">
<img src="/logo.svg" alt="" width="120" height="32" />
</a>
<ul role="list">
<li><a href="/components" aria-current="page">Components</a></li>
<li><a href="/tools">Tools</a></li>
<li><a href="/learn">Learn</a></li>
</ul>
</nav>
</header>
Key details:
aria-current="page" — Marks the link corresponding to the current page. Screen readers announce "current page". This is the definitive way to indicate active state.<nav> element — Provides landmark navigation. Screen reader users can jump directly to it.aria-label — Required when there are multiple <nav> elements ("Main navigation", "Footer navigation").aria-expanded — On the hamburger button, toggles between "true" and "false" to announce the mobile menu state.| Key | Action |
|---|---|
| Tab | Moves between navigation links |
| Enter | Activates the focused link |
| Escape | Closes the mobile menu (when open) |
| Arrow keys | Navigate within dropdown/mega menu submenus |
When the hamburger menu opens:
aria-expanded="true" on the toggle buttonoverflow: hidden on <body>)inert to page content behind the menuThis mirrors the Dialog accessibility pattern — a mobile nav menu IS essentially a dialog.
Check your navigation link contrast with the Contrast Checker — especially for transparent navbars where text overlays variable-color hero images.
aria-current="page" for the active link. Not just visual styling — programmatic identification matters for assistive tech.Find the perfect typography for your navbar with our Font Pair Tester.
<a href="#main-content" class="skip-link">Skip to main content</a>
<header class="navbar" role="banner">
<nav aria-label="Main navigation" class="navbar-inner">
<!-- Logo -->
<a href="/" class="navbar-logo" aria-label="Stellae Design — Home">
<img src="/logo.svg" alt="" width="120" height="32" />
</a>
<!-- Desktop links -->
<ul class="navbar-links" role="list">
<li><a href="/components" aria-current="page">Components</a></li>
<li><a href="/tools">Tools</a></li>
<li><a href="/learn">Learn</a></li>
<li><a href="/blog">Blog</a></li>
</ul>
<!-- Actions -->
<div class="navbar-actions">
<button type="button" class="btn btn-ghost btn-icon" aria-label="Search">
<svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11ZM2 9a7 7 0 1 1 12.452 4.391l3.328 3.329a.75.75 0 1 1-1.06 1.06l-3.329-3.328A7 7 0 0 1 2 9Z" clip-rule="evenodd"/>
</svg>
</button>
<a href="/login" class="btn btn-ghost">Log in</a>
<a href="/signup" class="btn btn-primary">Get started</a>
</div>
<!-- Mobile hamburger -->
<button
type="button"
class="navbar-hamburger"
aria-expanded="false"
aria-controls="mobile-menu"
aria-label="Open menu"
>
<svg aria-hidden="true" width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 6h18v2H3V6Zm0 5h18v2H3v-2Zm0 5h18v2H3v-2Z"/>
</svg>
</button>
</nav>
<!-- Mobile menu -->
<div id="mobile-menu" class="navbar-mobile-menu" hidden>
<ul role="list">
<li><a href="/components" aria-current="page">Components</a></li>
<li><a href="/tools">Tools</a></li>
<li><a href="/learn">Learn</a></li>
<li><a href="/blog">Blog</a></li>
</ul>
<div class="navbar-mobile-actions">
<a href="/login" class="btn btn-secondary btn-full">Log in</a>
<a href="/signup" class="btn btn-primary btn-full">Get started</a>
</div>
</div>
</header>
<main id="main-content"><!-- page content --></main>import { useState, useEffect, useRef, type ReactNode } from "react";
interface NavLink {
label: string;
href: string;
active?: boolean;
}
interface NavbarProps {
logo: ReactNode;
links: NavLink[];
actions?: ReactNode;
sticky?: boolean;
onNavigate?: (href: string) => void;
}
export default function Navbar({
logo,
links,
actions,
sticky = true,
onNavigate,
}: NavbarProps) {
const [mobileOpen, setMobileOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const toggleRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (!sticky) return;
const handleScroll = () => setScrolled(window.scrollY > 10);
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, [sticky]);
useEffect(() => {
if (!mobileOpen) return;
document.body.style.overflow = "hidden";
const handleEsc = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setMobileOpen(false);
toggleRef.current?.focus();
}
};
document.addEventListener("keydown", handleEsc);
return () => {
document.body.style.overflow = "";
document.removeEventListener("keydown", handleEsc);
};
}, [mobileOpen]);
const handleLinkClick = (href: string) => {
setMobileOpen(false);
onNavigate?.(href);
};
return (
<header className={`navbar ${sticky ? "navbar-sticky" : ""} ${scrolled ? "navbar-scrolled" : ""}`}>
<nav aria-label="Main navigation" className="navbar-inner">
<a href="/" className="navbar-logo" onClick={() => onNavigate?.("/")}>
{logo}
</a>
<ul className="navbar-links" role="list">
{links.map((link) => (
<li key={link.href}>
<a
href={link.href}
aria-current={link.active ? "page" : undefined}
onClick={(e) => { e.preventDefault(); handleLinkClick(link.href); }}
>
{link.label}
</a>
</li>
))}
</ul>
<div className="navbar-actions">{actions}</div>
<button
ref={toggleRef}
type="button"
className="navbar-hamburger"
aria-expanded={mobileOpen}
aria-controls="mobile-menu"
aria-label={mobileOpen ? "Close menu" : "Open menu"}
onClick={() => setMobileOpen(!mobileOpen)}
>
{mobileOpen ? "✕" : "☰"}
</button>
</nav>
{mobileOpen && (
<div ref={menuRef} id="mobile-menu" className="navbar-mobile-menu" role="dialog" aria-label="Navigation menu">
<ul role="list">
{links.map((link) => (
<li key={link.href}>
<a
href={link.href}
aria-current={link.active ? "page" : undefined}
onClick={(e) => { e.preventDefault(); handleLinkClick(link.href); }}
>
{link.label}
</a>
</li>
))}
</ul>
<div className="navbar-mobile-actions">{actions}</div>
</div>
)}
</header>
);
}
// Usage
<Navbar
logo={<img src="/logo.svg" alt="Stellae Design" width={120} height={32} />}
links={[
{ label: "Components", href: "/components", active: true },
{ label: "Tools", href: "/tools" },
{ label: "Learn", href: "/learn" },
{ label: "Blog", href: "/blog" },
]}
actions={
<>
<a href="/login" className="btn btn-ghost">Log in</a>
<a href="/signup" className="btn btn-primary">Get started</a>
</>
}
onNavigate={(href) => router.push(href)}
/>| Feature | Material 3 | Shadcn/ui | Radix | Ant Design |
|---|---|---|---|---|
| Component | Top App Bar (Small, Medium, Large) | No built-in navbar | NavigationMenu primitive | Layout.Header + Menu |
| Responsive | Built-in collapse to drawer | Manual implementation | N/A | Manual |
| Variants | Center-aligned, Small, Medium, Large | N/A | N/A | Fixed, transparent (manual) |
| Scroll behavior | Scroll off, fixed, compress | Manual | N/A | Manual |
| Mega menu | Not built-in | Not built-in | Full NavigationMenu with content areas | Not built-in |
| Active indicator | Color change + state layer | Manual | NavigationMenu.Indicator (animated underline) | selectedKeys prop |
| Mobile menu | Drawer component | Sheet component | N/A | Inline collapse |
Radix NavigationMenu stands apart from typical navbar implementations. Instead of a simple list of links, it provides a full primitive for accessible navigation menus with submenus, viewport-aware dropdown panels (mega menu style), and an animated indicator that slides between active items. It handles keyboard navigation, focus management, and the tricky "hovering between trigger and dropdown" problem where the dropdown shouldn't close as the user moves their cursor to it.
Material 3 differentiates between four Top App Bar sizes: Small (64px, compact), Medium (112px, prominent title), Large (152px, headline title), and Center-aligned (64px, centered title for simple pages). The larger variants shrink to Small on scroll — a polished effect that gives landing pages visual impact without permanently sacrificing space.
Shadcn/ui intentionally doesn't provide a navbar component. Navigation bars are too application-specific — every project needs different links, layouts, and responsive behavior. Instead, Shadcn provides the building blocks (NavigationMenu from Radix, Sheet for mobile, buttons, etc.) and lets you compose your own.
Ant Design uses Layout.Header + Menu composition. The Menu component handles horizontal/vertical mode switching, active state management, and submenu behavior. It's functional but visually opinionated — Ant's design language is strong, and overriding it requires effort.
Modern trend (2025–2026): Command Palette (⌘K) integration in navbars is becoming standard in developer-facing tools. Instead of (or alongside) traditional link navigation, users press a keyboard shortcut to open a searchable command palette that navigates to any page, triggers actions, or adjusts settings. Consider adding this as a complementary navigation pattern.