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 imperativelyconst 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 itemconst 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) MovementuseEffect(() => {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)}><navref={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"><ahref={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 TextactiveIndex === 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 LAYERSWe use CSS variables --spotlight-x and --ambience-x updated by JS*/}{/* 1. The Moving Spotlight (Follows Mouse) */}<divclassName="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) */}<divclassName="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 ColorsThis allows us to switch the gradient colors cleanly using Tailwind classeswithout 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"23export 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 Name | Type | Default | Description |
|---|---|---|---|
| items | NavItem[] | [{label: 'Home', href: '#'}, ...] | Array of navigation items with label and href. |
| className | string | undefined | Additional CSS classes to apply to the container. |
| onItemClick | (item: NavItem, index: number) => void | undefined | Callback function triggered when a nav item is clicked. |
| defaultActiveIndex | number | 0 | The index of the item that is active by default. |