DocumentationSpotlight Navbar

Spotlight Navbar

A navbar with a moving spotlight effect and active state ambience.

Install using CLI

npx shadcn@latest add "https://vengeance-ui.vercel.app/r/spotlight-navbar.json"

Install Manually

1

Install dependencies

npm install framer-motion clsx tailwind-merge
2

Add util file

lib/utils.ts

import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
3

Add styles to global CSS

Add the following styles to your app/globals.css file

/* Glass Component Borders */
.glass-border {
border: 1px solid rgba(0, 0, 0, 0.15);
}
.dark .glass-border {
border: 1px solid rgba(255, 255, 255, 0.2);
}
.spotlight-nav-bg {
background-color: rgb(250, 250, 250);
}
.dark .spotlight-nav-bg {
background-color: rgb(10, 10, 10);
}
.spotlight-nav-shadow {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.15), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
}
.dark .spotlight-nav-shadow {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -4px rgba(0, 0, 0, 0.5);
}
4

Copy the source code

Copy the code below and paste it 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;
}
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,
}: SpotlightNavbarProps) {
const navRef = useRef<HTMLDivElement>(null);
const [activeIndex, setActiveIndex] = useState(defaultActiveIndex);
const [hoverX, setHoverX] = useState<number | null>(null);
// Refs for the "light" positions so we can animate them imperatively
const spotlightX = useRef(0);
const ambienceX = useRef(0);
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);
// Direct update for immediate feedback (no spring for the mouse itself, feels snappier)
spotlightX.current = x;
nav.style.setProperty("--spotlight-x", `${x}px`);
};
const handleMouseLeave = () => {
setHoverX(null);
// When mouse leaves, spring the spotlight back to the active item
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]);
// Handle the "Ambience" (Active Item) Movement
useEffect(() => {
if (!navRef.current) return;
const nav = navRef.current;
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(ambienceX.current, targetX, {
type: "spring",
stiffness: 200,
damping: 20,
onUpdate: (v) => {
ambienceX.current = v;
nav.style.setProperty("--ambience-x", `${v}px`);
},
});
}
}, [activeIndex]);
const handleItemClick = (item: NavItem, index: number) => {
setActiveIndex(index);
onItemClick?.(item, index);
};
return (
<div className={cn("relative flex justify-center pt-10", 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"
)}
>
{/* Content */}
<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",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 dark:focus-visible:ring-white/30",
// Active vs Inactive Text
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>
{/* LIGHTING LAYERS
We use CSS variables --spotlight-x and --ambience-x updated by JS
*/}
{/* 1. The Moving Spotlight (Follows Mouse) */}
<div
className="pointer-events-none absolute bottom-0 left-0 w-full h-full z-[1] opacity-0 transition-opacity duration-300"
style={{
opacity: hoverX !== null ? 1 : 0,
background: `
radial-gradient(
120px circle at var(--spotlight-x) 100%,
var(--spotlight-color, rgba(0,0,0,0.1)) 0%,
transparent 50%
)
`
}}
/>
{/* 2. The Active State Ambience (Stays on Active) */}
<div
className="pointer-events-none absolute bottom-0 left-0 w-full h-[2px] z-[2]"
style={{
background: `
radial-gradient(
60px circle at var(--ambience-x) 0%,
var(--ambience-color, rgba(0,0,0,1)) 0%,
transparent 100%
)
`
}}
/>
</nav>
{/* STYLE BLOCK for Dynamic Colors
This allows us to switch the gradient colors cleanly using Tailwind classes
without messy inline conditionals.
*/}
<style jsx>{`
nav {
/* Light Mode Colors: Dark Gray/Black lights */
--spotlight-color: rgba(0,0,0,0.08);
--ambience-color: rgba(0,0,0,0.8);
}
:global(.dark) nav {
/* Dark Mode Colors: White lights */
--spotlight-color: rgba(255,255,255,0.15);
--ambience-color: rgba(255,255,255,1);
}
`}</style>
</div>
);
}

Usage

1import { SpotlightNavbar } from "@/components/ui/spotlight-navbar"
2
3export function NavbarDemo() {
4return (
5 <div className="w-full relative h-[600px] flex items-center justify-center bg-transparent">
6 <div className="absolute top-10">
7 <SpotlightNavbar />
8 </div>
9 </div>
10)
11}

Props

Prop NameTypeDefaultDescription
itemsNavItem[][{label: 'Home', href: '#'}, ...]Array of navigation items with label and href.
classNamestringundefinedAdditional CSS classes to apply to the container.
onItemClick(item: NavItem, index: number) => voidundefinedCallback function triggered when a nav item is clicked.
defaultActiveIndexnumber0The index of the item that is active by default.