Skip to content
Plinth

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
Open in v0
'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>
  );
}

More in Navigation

Similar tags