Header 1

Installation Steps

  1. Install Dependencies: Install the required dependencies using npm:

    npm install framer-motion
    

    Add required shadcn/ui components:

    npx shadcn@latest add button
    
  2. Copy this Component

Header.tsx
"use client";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { AnimatePresence, motion } from "framer-motion";
import { useRef, useState } from "react";
import { cn } from "@/lib/utils";

const Links = [
  { text: "Docs", href: "#" },
  { text: "Blog", href: "#" },
  { text: "Pricing", href: "#" },
];

const Logo = (): JSX.Element => {
  return (
    <div className="flex items-center gap-x-1 group hover:cursor-pointer">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        fill="currentColor"
        viewBox="0 0 24 24"
        strokeWidth={1.5}
        stroke="currentColor"
        className="size-5 sm:size-6 text-primary"
      >
        <path
          strokeLinecap="round"
          strokeLinejoin="round"
          d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z"
        />
      </svg>
      <span className="text-lg sm:text-xl font-semibold select-none group-hover:cursor-pointer text-background group-hover:text-muted-foreground dark:text-foreground dark:group-hover:text-muted-foreground transition-all">
        Optiq UI
      </span>
    </div>
  );
};

const NavLinkCursor = ({
  position,
}: {
  position: { left: number; width: number; opacity: number };
}): JSX.Element => {
  return (
    <motion.div
      className="absolute z-0 h-10 rounded-full bg-background dark:bg-secondary"
      animate={position}
      transition={{
        type: "spring",
        stiffness: 100,
        damping: 14,
        bounce: 0.05,
        duration: 0.1,
      }}
    />
  );
};

const NavBarLink = ({
  text,
  link,
  setPosition,
}: {
  text: string;
  link: string;
  setPosition: ({
    left,
    width,
    opacity,
  }: {
    left: number;
    width: number;
    opacity: number;
  }) => void;
}): JSX.Element => {
  const ref = useRef<HTMLAnchorElement>(null);
  return (
    <Link
      href={link}
      className="px-6 font-medium z-10 text-md relative mix-blend-exclusion text-background dark:text-foreground"
      ref={ref}
      onMouseEnter={() => {
        if (!ref.current) return;
        const { width } = ref.current.getBoundingClientRect();
        setPosition({
          width,
          opacity: 1,
          left: ref.current.offsetLeft,
        });
      }}
    >
      {text}
    </Link>
  );
};

const MobileMenu = ({
  isOpen,
  setIsOpen,
}: {
  isOpen: boolean;
  setIsOpen: (isOpen: boolean) => void;
}): JSX.Element => {
  const pathVariants = {
    closed: {
      d: "M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5",
    },
    open: {
      d: "M6 18L18 6M6 6l12 12",
    },
  };

  const menuVariants = {
    initial: {
      opacity: 0,
      scaleY: 0,
      transformOrigin: "top",
    },
    animate: {
      opacity: 1,
      scaleY: 1,
      transition: {
        duration: 0.2,
        ease: "easeInOut",
        when: "beforeChildren",
        staggerChildren: 0.5,
      },
    },
    exit: {
      opacity: 0,
      scaleY: 0,
      transition: {
        duration: 0.2,
        ease: "easeInOut",
        when: "afterChildren",
        staggerDirection: -1,
        staggerChildren: 0.5,
      },
    },
  };

  const linkVariants = {
    initial: {
      opacity: 0,
      x: -10,
    },
    animate: {
      opacity: 1,
      x: 0,
      transition: {
        duration: 0.3,
      },
    },
    exit: {
      opacity: 0,
      x: -10,
      transition: {
        duration: 0.3,
      },
    },
  };

  return (
    <motion.div
      className="flex justify-center items-center xl:hidden"
      animate={isOpen ? "open" : "closed"}
    >
      <motion.svg
        xmlns="http://www.w3.org/2000/svg"
        fill="none"
        viewBox="0 0 24 24"
        strokeWidth={1.5}
        stroke="currentColor"
        className="size-6 cursor-pointer"
        onClick={() => setIsOpen(!isOpen)}
        whileHover={{ scale: 1.1 }}
        whileTap={{ scale: 0.95 }}
      >
        <motion.path
          strokeLinecap="round"
          strokeLinejoin="round"
          variants={pathVariants}
          transition={{ duration: 0.3 }}
        />
      </motion.svg>
      <AnimatePresence mode="wait">
        {isOpen && (
          <motion.div
            className="dark:bg-neutral-900 dark:text-foreground bg-foreground text-background flex rounded-lg absolute top-16 backdrop-blur-sm inset-x-0 z-20 flex-col items-start justify-start w-full px-4 py-6"
            variants={menuVariants}
            initial="initial"
            animate="animate"
            exit="exit"
          >
            <motion.div
              className="flex flex-col w-full gap-4 px-4 text-base font-medium"
              variants={linkVariants}
            >
              {Links.map((item, index) => (
                <Link
                  href={item.href}
                  key={index}
                  className="hover:text-primary dark:hover:text-primary transition-all duration-300 hover:translate-x-2 text-start text-background dark:text-foreground"
                >
                  {item.text}
                </Link>
              ))}
              <div className="flex flex-col gap-y-2 w-full">
                <Button
                  variant="outline"
                  className="w-full px-6 text-secondary-foreground"
                >
                  Log in
                </Button>
                <Button variant="secondary" className="w-full px-6">
                  Sign up
                </Button>
              </div>
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>
    </motion.div>
  );
};

const Header = (): JSX.Element => {
  const [position, setPosition] = useState({ left: 0, width: 0, opacity: 0 });
  const [isOpen, setIsOpen] = useState(false);

  return (
    <header className="w-full px-4 sticky top-5 z-[60]">
      <div
        className={cn(
          "dark:bg-neutral-900 dark:text-foreground bg-foreground text-background flex flex-row justify-between items-center max-w-7xl mx-auto px-6 py-2.5 w-full inset-x-0 relative lg:rounded-full",
          { "rounded-full": !isOpen },
          { "rounded-lg": isOpen }
        )}
      >
        <div className="">
          <Link href={"#"}>
            <Logo />
          </Link>
        </div>
        <div
          className="hidden xl:flex items-center justify-center gap-x-4 relative h-full"
          onMouseLeave={() => setPosition((prev) => ({ ...prev, opacity: 0 }))}
        >
          {Links.map((item, index) => (
            <NavBarLink
              key={index}
              text={item.text}
              link={item.href}
              setPosition={setPosition}
            />
          ))}
          <NavLinkCursor position={position} />
        </div>
        <div className="hidden xl:flex items-center gap-x-2">
          <Button
            variant={"outline"}
            className="px-6 text-secondary-foreground"
          >
            Log in
          </Button>
          <Button variant={"secondary"} className="px-6">
            Sign up
          </Button>
        </div>
        <div className="flex justify-center items-center xl:hidden">
          <MobileMenu isOpen={isOpen} setIsOpen={setIsOpen} />
        </div>
      </div>
    </header>
  );
};

export { Header };