Skip to content

NavigationSite header

Site header

The global navigation. Brand on the left, links in the middle, mobile nav on the right.

Sticky on scrollActive-link stateMobile-aware

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.

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--foreground on hover
  • Underline: none. The active state is communicated by color alone.
  • After 24 px of scroll: --surface-elevated background with 88% opacity, backdrop-filter blur, 1 px --line-subtle bottom 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

PropTypeDefaultDescription
links{ label: string; href: string }[]NoneRequired: the nav items
transparentbooleanfalseTransparent at the top of the page, opaque on scroll
activePathstringNoneOverride 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-label that 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.