Original by Plinth · MIT
Navigation
Underline Nav
A nav bar whose underline glides to whichever link you hover.
#nav#underline#hover#javascript
Install
npx shadcn@latest add https://plinthui.com/r/underline-nav.json'use client';
import { useCallback, useEffect, useRef, useState } from 'react';
const LINKS = ['Work', 'About', 'Journal', 'Contact'];
const ACTIVE = 0;
export function UnderlineNav() {
const navRef = useRef<HTMLElement>(null);
const linkRefs = useRef<(HTMLAnchorElement | null)[]>([]);
const [rule, setRule] = useState({ left: 0, width: 0 });
// offsetLeft is measured against the nav, which is the offset parent.
const moveTo = useCallback((index: number) => {
const link = linkRefs.current[index];
if (!link) return;
setRule({ left: link.offsetLeft, width: link.offsetWidth });
}, []);
// Starts at zero width on the left edge, so it wipes open onto the active link.
// The rule is positioned from measured pixels, so it is re-measured whenever
// those pixels move: when the nav resizes, and once the webfont swaps in and
// the link widths change under it.
useEffect(() => {
moveTo(ACTIVE);
const nav = navRef.current;
const remeasure = () => moveTo(ACTIVE);
const observer = nav ? new ResizeObserver(remeasure) : null;
if (nav && observer) observer.observe(nav);
let cancelled = false;
document.fonts?.ready.then(() => {
if (!cancelled) remeasure();
});
return () => {
cancelled = true;
observer?.disconnect();
};
}, [moveTo]);
return (
<nav
ref={navRef}
aria-label="Primary"
onMouseLeave={() => moveTo(ACTIVE)}
onBlur={() => moveTo(ACTIVE)}
className="relative inline-flex items-center gap-7 pb-2.5 text-[15px]"
>
{LINKS.map((label, index) => (
<a
key={label}
href="#"
ref={el => {
linkRefs.current[index] = el;
}}
aria-current={index === ACTIVE ? 'page' : undefined}
onMouseEnter={() => moveTo(index)}
onFocus={() => moveTo(index)}
className="text-[#6f6d64] no-underline [transition:color_250ms_ease] hover:text-[#1a1a1a] focus-visible:text-[#1a1a1a] dark:text-[#8a8d96] dark:hover:text-[#ece9e2] dark:focus-visible:text-[#ece9e2]"
>
{label}
</a>
))}
<span
aria-hidden="true"
style={{
left: rule.left,
width: rule.width,
transition: 'left 250ms ease, width 250ms ease',
}}
className="absolute bottom-0 h-0.5 bg-[#8f7635] dark:bg-[#c8a96a]"
/>
</nav>
);
}