Navbar Hover

A simple Navbar hover effect

Navbar Hover

This is the hover effect where a soft background pill seems to slide from one nav link to the next instead of just blinking on and off under each one. It uses Framer Motion's shared layout animation: a single highlighted element is rendered behind whichever link is hovered, and Framer Motion animates it as it moves between positions. I reach for this on marketing-site headers and tab bars where I want the navigation to feel polished and continuous rather than abrupt.

import { useState } from "react";
import Link from "next/link";
import { AnimatePresence, motion } from "framer-motion";

export default function NavLinks() {
  let [hoveredIndex, setHoveredIndex] = useState(null);
  let links = [
    {
      name: "Home",
      link: "#home",
    },
    {
      name: "About",
      link: "#about",
    },
    {
      name: "Privacy",
      link: "#privacy",
    },
    {
      name: "Terms",
      link: "#terms",
    },
  ];

  return (
    <div className="mt-20 flex space-x-4 justify-center">
      {links.map((navLink, index) => (
        <Link key={navLink.name} href={navLink.link}>
          <a
            onMouseEnter={() => setHoveredIndex(index)}
            onMouseLeave={() => setHoveredIndex(null)}
            className="relative rounded-xl px-4 py-2 text-sm text-gray-700 transition-all delay-150 hover:text-gray-900"
          >
            <AnimatePresence>
              {hoveredIndex === index && (
                <motion.span
                  className="absolute inset-0 rounded-lg bg-gray-100"
                  layoutId="hoverBackground"
                  initial={{ opacity: 0 }}
                  animate={{ opacity: 1, transition: { duration: 0.15 } }}
                  exit={{
                    opacity: 0,
                    transition: { duration: 0.15, delay: 0.2 },
                  }}
                />
              )}
            </AnimatePresence>

            <span className="relative z-10">{navLink.name}</span>
          </a>
        </Link>
      ))}
    </div>
  );
}

How it works

The component tracks which link is hovered with a single piece of state, hoveredIndex. Each link's onMouseEnter sets the index and onMouseLeave resets it to null. When a link is hovered, it renders a motion.span positioned with absolute inset-0 so it fills that link, styled as a rounded gray background pill.

The magic is the shared layoutId="hoverBackground". Because every hovered link reuses the same layoutId, Framer Motion treats the pill as one continuous element: when the hovered index changes, instead of unmounting one pill and mounting another, it animates the existing pill's position and size from the old link to the new one, producing the gliding effect. AnimatePresence wraps the conditional pill so it can fade in and out when you enter or leave the nav entirely, the initial, animate, and exit opacity transitions handle that fade (note the slight delay on exit so the pill doesn't disappear before it finishes traveling). Finally, the link label is wrapped in a <span className="relative z-10"> so the text stacks above the absolutely positioned pill and stays readable.

Notes & variations

This depends on framer-motion (now Motion) and, as written, Next.js's Link plus Tailwind classes, but the core technique is just layoutId and is easy to port. The links here use in-page anchors (#home, #about), so swap those hrefs for your real routes. Recolor the pill by changing bg-gray-100, and adjust the 0.15s durations to make the slide snappier or smoother.

Two things to watch. This is a hover-driven effect, so it does nothing on touch devices where there's no hover, treat it as progressive enhancement and make sure the links are perfectly usable without it. For accessibility, the moving pill is purely decorative (the real text stays in normal flow and remains focusable through the Link/anchor), but if you also want a keyboard focus indicator you should add onFocus/onBlur handlers alongside the mouse ones, since this version only responds to the mouse.

Live Demo