Navbar

Animated expanding navbar that reveals links on scroll or hover with a smooth width and opacity transition inspired by Kumail Nanji.

Scroll down or hover to reveal the navbar

default.tsx
"use client";
 
import { useState, useEffect, useRef } from "react";
import {
  useMotionValue,
  useTransform,
  useMotionValueEvent,
  useScroll,
} from "motion/react";
import { cn } from "@/lib/utils";
import { AnimatePresence, motion } from "motion/react";
import { HugeiconsIcon } from "@hugeicons/react";
import { CloudIcon } from "@hugeicons/core-free-icons";
 
const ROUTES = [
  { title: "Home", href: "/" },
  { title: "About", href: "/about" },
  { title: "Benefits", href: "/contact" },
  { title: "Labs", href: "/contact" },
  { title: "contact", href: "/contact" },
];
 
const Nav = () => {
  const [isHidden, setIsHidden] = useState(false);
  const [height, setHeight] = useState(0);
  const [mounted, setMounted] = useState(false);
  const { scrollY } = useScroll();
  const lastYRef = useRef(0);
 
  const navbarWidth = useMotionValue(65);
  const routesOpacity = useTransform(navbarWidth, [65, 500], [0, 1]);
 
  useEffect(() => {
    setMounted(true);
  }, []);
 
  useMotionValueEvent(scrollY, "change", (y) => {
    const difference = y - lastYRef.current;
 
    if (difference > 50) {
      setIsHidden(false);
    } else {
      setIsHidden(true);
    }
 
    setHeight(difference);
  });
 
  const firstNavVariants = {
    hidden: {
      width: 65,
      background: "black",
    },
    vissible: {
      width: "auto",
      background: "black",
    },
  };
 
  if (!mounted) return null;
 
  return (
    <motion.header
      initial={{
        opacity: 0,
      }}
      animate={{
        opacity: 1,
      }}
      className="h-full w-full center md:block hidden"
      transition={{
        default: {
          ease: "easeInOut",
        },
        delay: 0.2,
        duration: 0.4,
      }}
    >
      <motion.nav
        animate={height > 50 && !isHidden ? "vissible" : "hidden"}
        whileHover="vissible"
        initial="hidden"
        exit="hidden"
        onFocusCapture={() => setIsHidden(false)}
        variants={firstNavVariants}
        transition={{
          default: {
            ease: "easeInOut",
          },
        }}
        className={cn(
          "fixed text-white z-500 h-16.25 min-w-16.25  backdrop-blur top-10 left-0 right-0 mx-auto overflow-hidden rounded-lg flex items-center max-w-fit ",
        )}
        style={{
          width: navbarWidth,
        }}
      >
        <div
          className={cn(
            "size-16.25 min-w-16.25 flex items-center justify-center",
          )}
        >
          <HugeiconsIcon icon={CloudIcon} className="size-8" />
        </div>
        <AnimatePresence>
          <div className="mr-10" key={"09q9q0"} />
          {(height >= 0 || !isHidden) && (
            <motion.ul className="flex items-center gap-10 list-none! mt-0!">
              {ROUTES.map((route, index) => (
                <motion.li
                  key={route.title}
                  className="text-white text-lg font-medium relative"
                  initial={{ opacity: 0 }}
                  animate={{ opacity: 1 }}
                  exit={{ opacity: 0 }}
                  style={{
                    opacity: routesOpacity,
                    marginRight: index === ROUTES.length - 1 ? "20px" : "0px",
                  }}
                >
                  {route.title}
                </motion.li>
              ))}
            </motion.ul>
          )}
        </AnimatePresence>
      </motion.nav>
    </motion.header>
  );
};
 
const SkalewayNav = () => {
  return (
    <div className="p-10 ">
      <Nav />
      <div className="flex items-center justify-center">
        <h1 className="text-muted-foreground text-xl text-center font-semibold">
          Scroll down or hover to reveal the navbar
        </h1>
      </div>
    </div>
  );
};
 
export default SkalewayNav;