GodUIGodUI
129Follow on X

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, with 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:

It uses the same 320/32/mass 0.9 spring as Mega Menu's panel morph. Both resize a real box, and both need 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 are independent: the hover pill can sweep past three links while the underline sits still on the active one:

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. With one nav, one threshold and 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 HeaderCC: Paint-triggering
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 →