Retour au catalogue
Navbar Animated Underline
Navbar ou le lien actif possede un souligne anime qui glisse fluidement d'un item a l'autre au survol.
navbarmedium Both Responsive a11y
minimalelegantuniversalagencysaassticky
Theme
"use client";
import { useState, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Menu, X } from "lucide-react";
interface NavLink {
label: string;
href: string;
}
interface NavbarAnimatedUnderlineProps {
brandName?: string;
links?: NavLink[];
ctaLabel?: string;
ctaUrl?: string;
}
export default function NavbarAnimatedUnderline({
brandName = "Brand",
links = [],
ctaLabel = "Contact",
ctaUrl = "#contact",
}: NavbarAnimatedUnderlineProps) {
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
const [mobileOpen, setMobileOpen] = useState(false);
const navRef = useRef<HTMLDivElement>(null);
return (
<>
<header
style={{
position: "relative",
zIndex: 50,
background: "var(--color-background)",
borderBottom: "1px solid var(--color-border)",
}}
>
<div
style={{
maxWidth: "var(--container-max-width)",
margin: "0 auto",
padding: "0 var(--container-padding-x)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
height: "60px",
}}
>
<a
href="/"
style={{
fontSize: "1.125rem",
fontWeight: 700,
color: "var(--color-foreground)",
textDecoration: "none",
letterSpacing: "-0.02em",
}}
>
{brandName}
</a>
{/* Desktop links with sliding underline */}
<div
ref={navRef}
style={{
display: "none",
alignItems: "center",
gap: "0.25rem",
position: "relative",
}}
className="lg:!flex"
onMouseLeave={() => setHoveredIdx(null)}
>
{links.map((link, i) => (
<a
key={link.href}
href={link.href}
onMouseEnter={() => setHoveredIdx(i)}
style={{
position: "relative",
fontSize: "0.875rem",
fontWeight: 500,
color: hoveredIdx === i ? "var(--color-foreground)" : "var(--color-foreground-muted)",
textDecoration: "none",
padding: "0.5rem 1rem",
transition: "color var(--duration-fast)",
}}
>
{link.label}
{hoveredIdx === i && (
<motion.span
layoutId="navbar-underline"
style={{
position: "absolute",
bottom: 0,
left: "1rem",
right: "1rem",
height: "2px",
background: "var(--color-accent)",
borderRadius: "var(--radius-full)",
}}
transition={{ type: "spring", stiffness: 380, damping: 30 }}
/>
)}
</a>
))}
</div>
<div style={{ display: "flex", alignItems: "center", gap: "0.75rem" }}>
<a
href={ctaUrl}
style={{
display: "none",
alignItems: "center",
padding: "0.5rem 1.25rem",
borderRadius: "var(--radius-full)",
background: "var(--color-accent)",
color: "var(--color-foreground)",
fontWeight: 600,
fontSize: "0.8125rem",
textDecoration: "none",
}}
className="lg:!inline-flex"
>
{ctaLabel}
</a>
<button
onClick={() => setMobileOpen(!mobileOpen)}
aria-label={mobileOpen ? "Fermer" : "Menu"}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "40px",
height: "40px",
border: "none",
background: "transparent",
cursor: "pointer",
color: "var(--color-foreground)",
}}
className="lg:!hidden"
>
{mobileOpen ? <X style={{ width: 20, height: 20 }} /> : <Menu style={{ width: 20, height: 20 }} />}
</button>
</div>
</div>
</header>
{/* Mobile */}
<AnimatePresence>
{mobileOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
style={{
overflow: "hidden",
position: "relative",
zIndex: 49,
background: "var(--color-background)",
borderBottom: "1px solid var(--color-border)",
}}
className="lg:!hidden"
>
<div
style={{
padding: "1rem var(--container-padding-x) 2rem",
display: "flex",
flexDirection: "column",
gap: "0.75rem",
}}
>
{links.map((link) => (
<a
key={link.href}
href={link.href}
onClick={() => setMobileOpen(false)}
style={{
fontSize: "1rem",
fontWeight: 500,
color: "var(--color-foreground)",
textDecoration: "none",
padding: "0.5rem 0",
borderBottom: "1px solid var(--color-border)",
}}
>
{link.label}
</a>
))}
<a
href={ctaUrl}
onClick={() => setMobileOpen(false)}
style={{
display: "flex",
justifyContent: "center",
marginTop: "0.5rem",
padding: "0.75rem",
borderRadius: "var(--radius-full)",
background: "var(--color-accent)",
color: "var(--color-foreground)",
fontWeight: 600,
textDecoration: "none",
}}
>
{ctaLabel}
</a>
</div>
</motion.div>
)}
</AnimatePresence>
</>
);
}