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>
    </>
  );
}

Avis

Navbar Animated Underline — React Navbar Section — Incubator