Header

The full-width bar at the top of every (site) page. Contains the logo/name lockup (Avatar + title), desktop nav links, a Contact CTA, and a mobile menu trigger that opens MobileNav. It owns the shared state for both navs.

Live Preview

The real Header rendered inline. In production it sits at the top of the page and scrolls away with it.

Composition

Position

Absolute, not fixed — it belongs to the top of the page rather than the viewport, so it scrolls away like any other content. Out of flow, though: every page's first section already carries top padding sized to clear it, and putting it back in flow would double that up and open a band of flat background above the home hero's gradient.

Two navs, one owner

Header owns pathname and the MobileNav open state, and renders three siblings: this bar, the FloatingNav pill, and MobileNav. Both navs share their markup through NavCluster, which takes a variant of bar or pill. MobileNav must stay a sibling of both — as a fixed-position element, it would break if either became its containing block.

Work mega menu

Above lg, the Work item is a split control: the link still navigates to /work, and an adjacent chevron button opens a panel of up to six case studies with a “See all projects” footer. Hover opens it after a short intent delay (gated on a fine pointer), click and Enter toggle it, Escape closes and returns focus to the chevron. It follows the ARIA disclosure pattern, not a menubar — these are links, and menu semantics would have screen readers announce them as menu items.

The panel is opaque rather than the usual .popover-surface glass. The floating pill sets a backdrop-filter, which makes it a backdrop root — a translucent panel nested inside would sample the pill's own fill instead of the page, and render see-through with no blur. It would look correct in the bar and wrong in the pill, in one variant only.

Fewer than four case studies and the whole thing degrades to a plain link — a two-column grid holding two items is worse than no menu. A failed CMS fetch lands there too. The preview below can't show the panel: it is clipped by this box, and the header renders here without case-study data.

Composition

Avatar (photo variant, rotating gradient border, 32×32) + name lockup at body-sm / body-xs; nav links driven by a shared list, active item matched on pathname; a primary “Contact now” button that replaces the Contact link on desktop. Below lg the links hide and a hamburger opens MobileNav.

Every link shares one hover treatment: the label lifts to --foreground and a rounded-full fill in --foreground-overlay appears behind it. Work carries that fill on its wrapper rather than its anchor, so the label and the chevron read as one control and the two overlays can't stack into a heavier shade. The current page is deliberately left out — a fill means “you are pointing at this”, the accent dot means “you are here”.

Services menu

Above lg the Services item is the same split control as Work, sharing one NavDropdown (hover intent, chevron, Escape, outside click, no re-open after navigating). The row owns a single open menu, so moving from Work to Services swaps the panels at once instead of waiting out the intent delay. The panel lists the four services in one column from the shared serviceList — the same titles, links, hues and illustrations as the services cards — each with the card's lead tick as its line, then “See all services” and a quiet free-call link.

The hover treatment reuses the system: each row (.nav-svc-row) carries its service's .svc-accent-*, so the fill tints in that hue and the arrow slides in in it; the illustration sits in a .illo-panel-thumb and plays its before → after story through .illo-hover, on hover and on keyboard focus. The thumbnail is each scene's compact version: the objects that tell its story kept whole, the rest dropped, so it reads at this size without cropping anything. Rows stagger in as the panel opens. In MobileNav the same rows sit under a chevron beside Services, collapsed by default, and play their story once seen. The rows, live:

Styling tokens

border-border-subtle + bg-[color-mix(in_srgb,var(--background)_60%,transparent)]px-4 sm:px-6 lg:px-8 gutter, h-8 row, max-w-[1360px] inner width

Import

tsx
import { Header } from '@/components/layout/Header'