NavigationSite header
Site header
The global navigation. Brand on the left, links in the middle, mobile nav on the right.
Demo
The header becomes opaque and acquires a 1 px line at the bottom after 24 px of scroll. The active link gets a left border on the doc system and a bottom border on the site.
Default
Anatomy
- Sticky to the top, full width, 64 px tall on desktop, 56 px on mobile
- Brand mark:
font-instrument-serif, 1.35 rem, -0.03 em tracking - Nav links: 0.8125 rem, weight 500, 0.04 em tracking
- Active link:
--foreground; inactive:--muted-foreground→--foregroundon hover - Underline: none. The active state is communicated by color alone.
- After 24 px of scroll:
--surface-elevatedbackground with 88% opacity, backdrop-filter blur, 1 px--line-subtlebottom border
Implementation
site-header.tsx
"use client"
import { useEffect, useState } from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { MobileNav } from "./mobile-nav"
interface NavLink { label: string; href: string }
export default function SiteHeader({ links }: { links: NavLink[] }) {
const pathname = usePathname()
const [scrolled, setScrolled] = useState(false)
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 24)
onScroll()
window.addEventListener("scroll", onScroll, { passive: true })
return () => window.removeEventListener("scroll", onScroll)
}, [])
return (
<header className={`site-header ${scrolled ? "site-header--scrolled" : ""}`} role="banner">
<div className="site-header__inner">
<Link href="/" className="site-header__brand font-serif" aria-label="izaias, home">
izaias
</Link>
<nav className="site-header__nav" aria-label="Main navigation">
{links.map(({ href, label }) => {
const isActive = href === "/" ? pathname === "/" : pathname === href || pathname.startsWith(`${href}/`)
return (
<Link
key={href}
href={href}
className={`site-header__link ${isActive ? "site-header__link--active" : ""}`}
aria-current={isActive ? "page" : undefined}
>
{label}
</Link>
)
})}
</nav>
<MobileNav links={links} />
</div>
</header>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
links | { label: string; href: string }[] | None | Required: the nav items |
transparent | boolean | false | Transparent at the top of the page, opaque on scroll |
activePath | string | None | Override the active link |
Accessibility
- The header is a
role="banner"landmark. - The nav has
aria-label="Main navigation". - Active link is announced with
aria-current="page". - The brand link has an
aria-labelthat includes the destination, not just the brand mark. - Skip link: a hidden "Skip to main content" link is the first focusable element on every page.