MynduiMyndui
0

Anatomy of the Resizable Header

How one nav bar morphs from a wide rounded surface into a floating pill on scroll, and carries two independent layoutId tracks for hover and active state.

01/04One nav: logo, links, CTA
Logo
brand mark, shrink-0 on the left
Links
nav row — carries the hover pill + active underline
CTA
shrink-0 action slot on the right
tsx
<motion.navlayouttransition={spring}className={`... border border-border bg-background/70 backdrop-blur-xl ${  shrunk    ? "w-full max-w-2xl rounded-full px-3 py-2 shadow-lg"    : "w-full max-w-5xl rounded-2xl px-4 py-3 shadow-sm"}`}/>
01

One nav: logo, links, CTA

Most "shrinking header" implementations swap two separate DOM trees at a breakpoint. Resizable Header is one motion.nav the whole time — a single element whose class names change and whose box framer FLIPs between them, so the shrink reads as one continuous move instead of a cut.

Structurally it's a <header> wrapping a single motion.nav — a shrink-0 logo on the left, a link row in the middle with room for a hover pill and an active underline, and a shrink-0 CTA plus mobile toggle on the right:

02/04Scroll threshold morph
Expanded
max-w-5xl · rounded-2xl · scrollY ≤ threshold
Collapsed
max-w-2xl · rounded-full pill · past threshold
tsx
const { scrollY } = useScroll(scrollRef ? { container: scrollRef } : undefined);useMotionValueEvent(scrollY, "change", (latest) => {setShrunk(latest > scrollThreshold);});const spring = { type: "spring", stiffness: 320, damping: 32, mass: 0.9 };
02

Scroll threshold morph

useScroll tracks scrollY; a useMotionValueEvent listener flips a single boolean the instant it passes scrollThreshold (default 24px). That boolean is the only thing driving the two class strings above — the layout prop on motion.nav does the rest, FLIPping the box from max-w-5xl/rounded-2xl to max-w-2xl/rounded-full on a spring instead of snapping:

The same 320/32/mass 0.9 spring as Mega Menu's panel morph — both are "resize a real box" animations, and both want that extra mass so a wide surface doesn't overshoot visibly on the way to its new width.

03/04Two layoutId tracks
Hover pill
layoutId span, fills the item under the pointer
Active underline
separate layoutId — tracks the clicked link only
tsx
{hovered === link.href && (<motion.span layoutId={`${ids.pill}-hover`} transition={spring} className="... bg-accent" />)}{isActive && (<motion.span layoutId={ids.pill} transition={spring} className="... bg-foreground" />)}
03

Two layoutId tracks

The link row carries two independent motion.spans, each bound to its own layoutId: a hover pill that fills whatever link the pointer is over, and a thin active underline that only follows a click. They don't know about each other — the hover pill can sweep past three links while the underline sits still on the one that's actually active:

sticky (default true) pins the header with sticky top-0 z-sticky; set it false for a header that scrolls away normally. For layouts where the page itself isn't the scrolling element — a modal body, a split-pane panel — pass scrollRef and useScroll tracks that container's scroll position instead of the window's. Below md, the link row and CTA hide entirely behind a hamburger button that opens an AnimatePresence dropdown sharing the header's border radius and shadow language.

04/04Result
northwind.com

Ship faster with Northwind

Scroll this panel — the nav springs into a compact, blurred pill and the active-link indicator rides along.

Analytics
Automations
Integrations
Security
04

The result

Scroll it: one nav, one threshold, one spring — the bar becomes a pill and the two highlight tracks keep riding along underneath it.

Accessibility

The active link carries aria-current="page", every link gets a focus-visible ring offset from the background so it reads clearly against the blurred surface, and the mobile toggle exposes aria-expanded so assistive tech knows the drawer's state without relying on the icon morph alone.

Motion Score

Resizable HeaderSSCompositor-only
SopacityFade / cross-fade
Each property is graded by how the browser runs it, from S (composited off the main thread) down to F (layout thrashing); the component takes the worst. MotionScore methodology →