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
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 Name | Type | Default | Description |
|---|---|---|---|
| items | NavItem[] | [...defaultItems] | Array of navigation items with label and href |
| className | string | - | Additional CSS classes |
| onItemClick | (item, index) => void | - | Callback when an item is clicked |
| defaultActiveIndex | number | 0 | Initially active item index |
| logo | React.ReactNode | - | Optional logo to display in center |
Examples
With Logo
<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" />}
/>