Docs
Floating Navabar
Floating Navabar
This component provides an elegant navigation menu with animated dropdown panels that appear when hovering over menu items.
The Navbar will show on top of the page
Install the following dependencies:
pnpm add gsap @gsap/react
Make a file for cn and mergerRef functions and match the import afterwards
import clsx, { ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export const cn = (...classes: ClassValue[]) => twMerge(clsx(...classes))
Make a file and copy paste this code in a file with name
floating-navbar.tsx
"use client"
import React, {
useRef,
useEffect,
useState,
useCallback,
ReactElement,
AnchorHTMLAttributes,
} from "react"
import gsap from "gsap"
import { cn } from "@/lib/utils"
// Types for better code organization
type MenuItemProps = {
item: string
children?: React.ReactNode
}
type EnhancedMenuItemProps = MenuItemProps & {
onMouseEnter?: () => void
isActive?: boolean
isAnimatingOut?: boolean
registerDropdownRef?: (ref: HTMLDivElement | null) => void
animateDropdownIn?: (ref: HTMLDivElement) => void
}
type MenuProps = {
children: React.ReactNode
}
// The main Menu component that controls all state
const Menu = ({ children }: MenuProps) => {
const [activeItem, setActiveItem] = useState<string | null>(null)
const [animatingOutItem, setAnimatingOutItem] = useState<string | null>(null)
const dropdownRefs = useRef<Map<string, HTMLDivElement>>(new Map())
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
// Clear any existing timeouts when component unmounts
useEffect(() => {
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
}, [])
// Handle mouse enter on a menu item
const handleItemMouseEnter = useCallback(
(item: string) => {
// Clear any ongoing animations/timeouts
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
timeoutRef.current = null
}
// If we're animating out the same item that's being hovered,
// just cancel the animation out
if (animatingOutItem === item) {
setAnimatingOutItem(null)
}
// If we're hovering a different item, animate the current one out first
if (
activeItem &&
activeItem !== item &&
animatingOutItem !== activeItem
) {
const prevDropdown = dropdownRefs.current.get(activeItem)
if (prevDropdown) {
// Animate out the previously active dropdown
gsap.to(prevDropdown, {
opacity: 0,
scale: 0.85,
y: 10,
duration: 0.3,
ease: "power2.out",
onComplete: () => {
// After animating out, set the new active item
setActiveItem(item)
setAnimatingOutItem(null)
},
})
setAnimatingOutItem(activeItem)
return
}
}
// Set the new active item directly if no animation needed
setActiveItem(item)
},
[activeItem, animatingOutItem]
)
// Handle mouse leave on the entire menu
const handleMenuMouseLeave = useCallback(() => {
if (activeItem) {
const dropdown = dropdownRefs.current.get(activeItem)
if (dropdown) {
// Animate out the active dropdown
gsap.to(dropdown, {
opacity: 0,
scale: 0.85,
y: 10,
duration: 0.4,
ease: "power3.out",
onComplete: () => {
setActiveItem(null)
setAnimatingOutItem(null)
},
})
setAnimatingOutItem(activeItem)
} else {
// Fallback if ref is not available
timeoutRef.current = setTimeout(() => {
setActiveItem(null)
setAnimatingOutItem(null)
}, 400)
}
}
}, [activeItem])
// Register a dropdown ref
const registerDropdownRef = useCallback(
(item: string, ref: HTMLDivElement | null) => {
if (ref) {
dropdownRefs.current.set(item, ref)
} else {
dropdownRefs.current.delete(item)
}
},
[]
)
// Animate in a dropdown when it becomes active
const animateDropdownIn = useCallback((ref: HTMLDivElement) => {
gsap.set(ref, { opacity: 0, scale: 0.85, y: 10 })
gsap.to(ref, {
opacity: 1,
scale: 1,
y: 0,
duration: 0.5,
ease: "back.out(1.7)",
})
}, [])
// Clone children to add necessary props
const enhancedChildren = React.Children.map(children, (child) => {
if (React.isValidElement<MenuItemProps>(child)) {
const item = child.props.item
// Only pass valid props that are defined in the child component's prop type
const enhancedProps: EnhancedMenuItemProps = {
...child.props,
onMouseEnter: () => handleItemMouseEnter(item),
isActive: activeItem === item,
isAnimatingOut: animatingOutItem === item,
registerDropdownRef: (ref: HTMLDivElement | null) =>
registerDropdownRef(item, ref),
animateDropdownIn,
}
return React.cloneElement(child, enhancedProps)
}
return child
})
return (
<nav
onMouseLeave={handleMenuMouseLeave}
className="relative rounded-full border border-transparent dark:bg-black dark:border-white/[0.2] bg-white shadow-input flex justify-center space-x-4 px-8 py-6"
>
{enhancedChildren}
</nav>
)
}
// MenuItem is now a controlled component that receives all it needs from Menu
const MenuItem = React.forwardRef<HTMLDivElement, EnhancedMenuItemProps>(
(
{
item,
children,
onMouseEnter,
isActive = false,
isAnimatingOut = false,
registerDropdownRef,
animateDropdownIn,
},
_ref
) => {
const menuItemRef = useRef<HTMLParagraphElement>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
// Register the dropdown ref when it's created
useEffect(() => {
if (dropdownRef.current && registerDropdownRef) {
registerDropdownRef(dropdownRef.current)
// Return cleanup function
return () => {
registerDropdownRef(null)
}
}
}, [registerDropdownRef])
// Animate dropdown in when it becomes active
useEffect(() => {
if (
isActive &&
dropdownRef.current &&
animateDropdownIn &&
!isAnimatingOut
) {
animateDropdownIn(dropdownRef.current)
}
}, [isActive, animateDropdownIn, isAnimatingOut])
return (
<div onMouseEnter={onMouseEnter} className="relative">
<p
ref={menuItemRef}
className="cursor-pointer text-black hover:opacity-[0.9] dark:text-white"
>
{item}
</p>
{/* Show dropdown when active OR when animating out */}
{(isActive || isAnimatingOut) && (
<div className="absolute top-[calc(100%_+_1.2rem)] left-1/2 transform -translate-x-1/2 pt-4">
<div
ref={dropdownRef}
className="bg-white dark:bg-black backdrop-blur-sm rounded-2xl overflow-hidden border border-black/[0.2] dark:border-white/[0.2] shadow-xl"
>
<div className="w-max h-full p-4">{children}</div>
</div>
</div>
)}
</div>
)
}
)
MenuItem.displayName = "MenuItem"
// Unchanged components
const ProductItem = ({
title,
description,
href,
src,
}: {
title: string
description: string
href: string
src: string
}) => {
return (
<a href={href} className="flex space-x-2">
<img
src={src}
width={140}
height={70}
alt={title}
className="shrink-0 rounded-md shadow-2xl"
/>
<div>
<h4 className="text-base md:text-xl font-bold mb-1 text-black dark:text-white">
{title}
</h4>
<p className="text-neutral-700 text-xs md:text-sm max-w-[10rem] dark:text-neutral-300">
{description}
</p>
</div>
</a>
)
}
interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
children: ReactElement | string
}
const HoveredLink = ({ children, className = "", ...rest }: LinkProps) => {
return (
<a
{...rest}
className={cn(
"text-neutral-700 dark:text-neutral-200 hover:text-black",
className
)}
>
{children}
</a>
)
}
export { Menu, MenuItem, ProductItem, HoveredLink }