DocumentationSpotlight Navbar

Spotlight Navbar

A stunning navigation bar with a mouse-following spotlight effect and active state indicator. The spotlight creates an elegant light trail that follows the cursor, while a persistent glow highlights the active item.

Install using CLI

npx shadcn@latest add "https://obsidianui.dev/r/spotlight-navbar.json"

Install Manually

1

Install dependencies

npm install framer-motion clsx tailwind-merge
2

Add CSS styles

Add these styles to your globals.css:

.spotlight-nav-bg {
  background: linear-gradient(to bottom, 
    rgba(255,255,255,0.9) 0%, 
    rgba(255,255,255,0.7) 100%
  );
}
 
.dark .spotlight-nav-bg {
  background: linear-gradient(to bottom, 
    rgba(30,30,30,0.9) 0%, 
    rgba(20,20,20,0.7) 100%
  );
}
 
.glass-border {
  border: 1px solid rgba(0,0,0,0.1);
}
 
.dark .glass-border {
  border: 1px solid rgba(255,255,255,0.1);
}
 
.spotlight-nav-shadow {
  box-shadow: 
    0 4px 6px -1px rgba(0,0,0,0.1), 
    0 2px 4px -1px rgba(0,0,0,0.06);
}
 
.dark .spotlight-nav-shadow {
  box-shadow: 
    0 4px 6px -1px rgba(0,0,0,0.3), 
    0 2px 4px -1px rgba(0,0,0,0.2);
}
3

Copy the source code

Copy into components/ui/spotlight-navbar.tsx

"use client";
 
import React, { useEffect, useRef, useState } from "react";
import { animate } from "framer-motion";
import { cn } from "@/lib/utils";
 
export interface NavItem {
    label: string;
    href: string;
}
 
export interface SpotlightNavbarProps {
    items?: NavItem[];
    className?: string;
    onItemClick?: (item: NavItem, index: number) => void;
    defaultActiveIndex?: number;
    logo?: React.ReactNode;
}
 
export function SpotlightNavbar({
    items = [
        { label: "Home", href: "#home" },
        { label: "About", href: "#about" },
        { label: "Events", href: "#events" },
        { label: "Sponsors", href: "#sponsors" },
        { label: "Pricing", href: "#pricing" },
    ],
    className,
    onItemClick,
    defaultActiveIndex = 0,
    logo,
}: SpotlightNavbarProps) {
    const navRef = useRef<HTMLDivElement>(null);
    const [activeIndex, setActiveIndex] = useState(defaultActiveIndex);
    const [hoverX, setHoverX] = useState<number | null>(null);
    const [isDark, setIsDark] = useState(false);
    const spotlightX = useRef(0);
    const ambienceX = useRef(0);
 
    useEffect(() => {
        const checkTheme = () => {
            setIsDark(document.documentElement.classList.contains('dark'));
        };
        checkTheme();
        const observer = new MutationObserver(checkTheme);
        observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
        return () => observer.disconnect();
    }, []);
 
    useEffect(() => {
        if (!navRef.current) return;
        const nav = navRef.current;
 
        const handleMouseMove = (e: MouseEvent) => {
            const rect = nav.getBoundingClientRect();
            const x = e.clientX - rect.left;
            setHoverX(x);
            spotlightX.current = x;
            nav.style.setProperty("--spotlight-x", `${x}px`);
        };
 
        const handleMouseLeave = () => {
            setHoverX(null);
            const activeItem = nav.querySelector(`[data-index="${activeIndex}"]`);
            if (activeItem) {
                const navRect = nav.getBoundingClientRect();
                const itemRect = activeItem.getBoundingClientRect();
                const targetX = itemRect.left - navRect.left + itemRect.width / 2;
 
                animate(spotlightX.current, targetX, {
                    type: "spring",
                    stiffness: 200,
                    damping: 20,
                    onUpdate: (v) => {
                        spotlightX.current = v;
                        nav.style.setProperty("--spotlight-x", `${v}px`);
                    }
                });
            }
        };
 
        nav.addEventListener("mousemove", handleMouseMove);
        nav.addEventListener("mouseleave", handleMouseLeave);
 
        return () => {
            nav.removeEventListener("mousemove", handleMouseMove);
            nav.removeEventListener("mouseleave", handleMouseLeave);
        };
    }, [activeIndex]);
 
    useEffect(() => {
        if (!navRef.current) return;
        const nav = navRef.current;
 
        const updatePosition = () => {
            const activeItem = nav.querySelector(`[data-index="${activeIndex}"]`);
            if (activeItem) {
                const navRect = nav.getBoundingClientRect();
                const itemRect = activeItem.getBoundingClientRect();
                const targetX = itemRect.left - navRect.left + itemRect.width / 2;
 
                if (ambienceX.current === 0) {
                    ambienceX.current = targetX;
                    nav.style.setProperty("--ambience-x", `${targetX}px`);
                } else {
                    animate(ambienceX.current, targetX, {
                        type: "spring",
                        stiffness: 200,
                        damping: 20,
                        onUpdate: (v) => {
                            ambienceX.current = v;
                            nav.style.setProperty("--ambience-x", `${v}px`);
                        },
                    });
                }
            }
        };
 
        requestAnimationFrame(updatePosition);
    }, [activeIndex]);
 
    const handleItemClick = (item: NavItem, index: number) => {
        setActiveIndex(index);
        onItemClick?.(item, index);
        if (item.href.startsWith('#')) {
            const element = document.querySelector(item.href);
            element?.scrollIntoView({ behavior: 'smooth' });
        } else {
            window.location.href = item.href;
        }
    };
 
    return (
        <div className={cn("relative flex justify-center pt-4", className)}>
            <nav
                ref={navRef}
                className={cn(
                    "spotlight-nav spotlight-nav-bg glass-border spotlight-nav-shadow",
                    "relative h-11 rounded-full transition-all duration-300 overflow-hidden"
                )}
            >
                <ul className="relative flex items-center h-full px-2 gap-0 z-[10]">
                    {items.map((item, idx) => (
                        <li key={idx} className="relative h-full flex items-center justify-center">
                            <a
                                href={item.href}
                                data-index={idx}
                                onClick={(e) => {
                                    e.preventDefault();
                                    handleItemClick(item, idx);
                                }}
                                className={cn(
                                    "px-4 py-2 text-sm font-medium transition-colors duration-200 rounded-full",
                                    activeIndex === idx
                                        ? "text-black dark:text-white"
                                        : "text-neutral-500 dark:text-neutral-400 hover:text-black dark:hover:text-white"
                                )}
                            >
                                {item.label}
                            </a>
                        </li>
                    ))}
                </ul>
 
                <div
                    className="pointer-events-none absolute bottom-0 left-0 w-full h-full z-[1]"
                    style={{
                        opacity: hoverX !== null ? 1 : 0,
                        background: `radial-gradient(120px circle at var(--spotlight-x) 100%, var(--spotlight-color) 0%, transparent 50%)`
                    }}
                />
 
                <div
                    className="pointer-events-none absolute bottom-0 left-0 w-full h-[3px] z-[2]"
                    style={{
                        background: `radial-gradient(80px circle at var(--ambience-x) 0%, var(--ambience-color) 0%, transparent 100%)`
                    }}
                />
 
            </nav>
 
            <style jsx>{`
                nav {
                    --spotlight-color: rgba(0,0,0,0.08);
                    --ambience-color: rgba(0,0,0,0.8);
                }
                :global(.dark) nav {
                    --spotlight-color: rgba(255,255,255,0.15);
                    --ambience-color: rgba(255,255,255,1);
                }
            `}</style>
        </div>
    );
}
 
export default SpotlightNavbar;

Props

Prop NameTypeDefaultDescription
itemsNavItem[][...defaultItems]Array of navigation items with label and href
classNamestring-Additional CSS classes
onItemClick(item, index) => void-Callback when an item is clicked
defaultActiveIndexnumber0Initially active item index
logoReact.ReactNode-Optional logo to display in center

Examples

<SpotlightNavbar 
  items={[
    { label: "Home", href: "/" },
    { label: "About", href: "/about" },
    { label: "Contact", href: "/contact" },
  ]}
  logo={<img src="/logo.svg" alt="Logo" className="h-6 w-6" />}
/>