Navbar
A responsive navigation bar with scroll effects and mobile drawer.
Overview
A polished navigation component with scroll-aware styling, animated logo, theme toggle, and responsive mobile drawer. Features a sliding tab indicator for active navigation items.
Features
- Scroll-aware background and sizing
- Animated logo with bouncing blocks
- Sliding tab indicator on hover
- Mobile drawer with smooth animations
- Built-in theme switcher
Preview
Installation
Method 1: CLI (Recommended)
Install via the ZenBlocks CLI to automatically handle dependencies:
npx shadcn@latest add https://zenblocks-three.vercel.app/r/navbar.jsonNote: The CLI does not set up the global
ThemeProvider. Please see the Configuration guide to set up dark mode support.
Method 2: Manual
-
Install Dependencies
npm install framer-motion lucide-react next-themes -
Setup Theme Provider
This component uses
next-themes. Ensure your app is wrapped in aThemeProvideras described in the Global Installation guide. -
Copy the Source Code
Copy the code below into
components/zenblocks/navbar.tsx.Click to expand source
"use client"; import React, { useEffect, useRef, useState } from "react"; import { Moon, Sun, Menu, X } from "lucide-react"; import { motion, useReducedMotion } from "framer-motion"; import Link from "next/link"; import { useTheme } from "next-themes"; import { cn } from "@/lib/utils"; /* -------------------------------------------------------------------------- */ /* TYPES */ /* -------------------------------------------------------------------------- */ export type NavbarItem = { label: string; href: string; }; type NavbarProps = { position?: "fixed" | "relative"; items?: NavbarItem[]; className?: string; logo?: React.ReactNode; brandText?: React.ReactNode; brandClassName?: string; }; const DEFAULT_NAV_ITEMS: NavbarItem[] = [ { label: "Docs", href: "/docs" }, { label: "Components", href: "/docs/components" }, { label: "Templates", href: "/templates" }, { label: "Pricing", href: "/pricing" }, ]; /* -------------------------------------------------------------------------- */ /* NAVBAR */ /* -------------------------------------------------------------------------- */ export default function Navbar({ position = "fixed", items = DEFAULT_NAV_ITEMS, className, logo, brandText = "ZENBLOCKS", brandClassName }: NavbarProps) { const [scrolled, setScrolled] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const prefersReduced = useReducedMotion(); useEffect(() => { if (position === "relative") return; const onScroll = () => setScrolled(window.scrollY > 50); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, [position]); const navLinks = items; return ( <div className={cn( "z-50 px-3 mt-3 pointer-events-none", position === "fixed" ? "fixed top-0 left-0 right-0 flex justify-center" : "relative w-full" )} > <nav aria-label="Main Navigation" className={cn( "pointer-events-auto w-full rounded-2xl px-4 py-2 border transition-all duration-300", position === "fixed" ? "max-w-3xl mx-auto" : "max-w-full", scrolled && position === "fixed" ? "bg-white/50 dark:bg-zinc-900/50 backdrop-blur-xl shadow-md border-zinc-200/50 dark:border-white/10" : "bg-white/10 dark:bg-zinc-900/10 backdrop-blur-lg border-zinc-900/5 dark:border-white/5", "text-zinc-900 dark:text-white", // Default text color className )} > <div className="relative flex items-center h-10"> {/* LOGO */} <div className="absolute left-0 flex items-center gap-2"> <Link href="/" className="flex items-center gap-2"> {logo ?? <CompactLogo prefersReduced={!!prefersReduced} />} <span className={cn("text-sm font-semibold tracking-tight text-current", brandClassName)}> {brandText} </span> </Link> </div> {/* CENTER NAV */} <div className="flex-1 flex justify-center"> <div className="hidden sm:block"> <SlideTabs navLinks={navLinks} prefersReduced={!!prefersReduced} /> </div> </div> {/* RIGHT CONTROLS */} <div className="absolute right-0 flex items-center gap-2"> <ThemeButton /> <button onClick={() => setMobileMenuOpen((s) => !s)} className="sm:hidden p-1.5 rounded-md hover:bg-black/5 dark:hover:bg-white/5 transition" aria-label="Toggle menu" > {mobileMenuOpen ? ( <X className="w-5 h-5 text-current" /> ) : ( <Menu className="w-5 h-5 text-current" /> )} </button> </div> </div> {/* MOBILE MENU */} {mobileMenuOpen && ( <div className="sm:hidden mt-3 pt-3 border-t border-black/10 dark:border-white/10 flex flex-col gap-2 bg-transparent"> {navLinks.map((link) => ( <Link key={link.href} href={link.href} onClick={() => setMobileMenuOpen(false)} className="px-3 py-2 rounded-md text-sm font-medium text-current hover:bg-black/5 dark:hover:bg-white/5" > {link.label} </Link> ))} </div> )} </nav> </div> ); } /* -------------------------------------------------------------------------- */ /* LOGO */ /* -------------------------------------------------------------------------- */ function CompactLogo({ prefersReduced }: { prefersReduced: boolean }) { const blocks = [ "bg-neutral-900 dark:bg-neutral-300", "bg-neutral-600 dark:bg-neutral-500", "bg-neutral-400 dark:bg-neutral-700", ]; return ( <div className="flex gap-0.5 w-5 h-5 items-center"> {blocks.map((cls, i) => ( <motion.div key={i} className={`w-2 h-2 rounded-sm ${cls}`} animate={prefersReduced ? undefined : { y: [0, -3, 0] }} transition={{ duration: 1.1, repeat: Infinity, ease: "easeInOut", delay: i * 0.12, }} /> ))} </div> ); } /* -------------------------------------------------------------------------- */ /* THEME BUTTON */ /* -------------------------------------------------------------------------- */ function ThemeButton() { const { setTheme, resolvedTheme } = useTheme(); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); if (!mounted) { return ( <div className="w-8 h-8 rounded-md border border-zinc-200 dark:border-zinc-800" /> ); } const isDark = resolvedTheme === "dark"; return ( <button onClick={() => setTheme(isDark ? "light" : "dark")} className="relative w-9 h-9 flex items-center justify-center rounded-xl transition-all duration-300 bg-zinc-100 dark:bg-zinc-800/50 hover:bg-zinc-200 dark:hover:bg-zinc-800 border border-zinc-200 dark:border-zinc-700/50" aria-label="Toggle theme" > <div className="relative w-5 h-5 overflow-hidden"> <motion.div animate={{ y: isDark ? 0 : 25, opacity: isDark ? 1 : 0, rotate: isDark ? 0 : -45, }} transition={{ type: "spring", stiffness: 300, damping: 20 }} className="absolute inset-0 flex items-center justify-center" > <Sun className="w-5 h-5 text-amber-400 fill-amber-400/20" /> </motion.div> <motion.div animate={{ y: isDark ? -25 : 0, opacity: isDark ? 0 : 1, rotate: isDark ? 45 : 0, }} transition={{ type: "spring", stiffness: 300, damping: 20 }} className="absolute inset-0 flex items-center justify-center" > <Moon className="w-5 h-5 text-zinc-900 fill-zinc-900/10" /> </motion.div> </div> {/* Subtle Glow */} <motion.div animate={{ opacity: isDark ? 0.5 : 0, }} className="absolute inset-0 rounded-xl bg-amber-400/20 blur-md pointer-events-none" /> </button> ); } /* -------------------------------------------------------------------------- */ /* SLIDE TABS */ /* -------------------------------------------------------------------------- */ function SlideTabs({ navLinks, prefersReduced, }: { navLinks: { label: string; href: string }[]; prefersReduced: boolean; }) { const containerRef = useRef<HTMLUListElement | null>(null); const [position, setPosition] = useState({ left: 0, width: 0, opacity: 0, }); return ( <ul ref={containerRef} onMouseLeave={() => setPosition((p) => ({ ...p, opacity: 0 }))} className="relative flex items-center h-9 px-1 rounded-full" role="menubar" > {navLinks.map((link) => ( <CompactTab key={link.href} href={link.href} containerRef={containerRef} setPosition={setPosition} > {link.label} </CompactTab> ))} <motion.li aria-hidden animate={position} transition={ prefersReduced ? { duration: 0 } : { type: "spring", stiffness: 380, damping: 32 } } className="absolute top-0 h-9 rounded-full bg-zinc-900 dark:bg-zinc-100/10" /> </ul> ); } /* -------------------------------------------------------------------------- */ /* TAB ITEM */ /* -------------------------------------------------------------------------- */ interface CompactTabProps { children: React.ReactNode; href: string; containerRef: React.RefObject<HTMLUListElement | null>; setPosition: React.Dispatch< React.SetStateAction<{ left: number; width: number; opacity: number }> >; } const CompactTab: React.FC<CompactTabProps> = ({ children, href, containerRef, setPosition, }) => { const ref = useRef<HTMLAnchorElement | null>(null); const update = () => { if (!ref.current || !containerRef.current) return; const rect = ref.current.getBoundingClientRect(); const parent = containerRef.current.getBoundingClientRect(); setPosition({ left: rect.left - parent.left, width: rect.width, opacity: 1, }); }; return ( <li className="relative z-10 h-9 flex items-center" role="none"> <Link ref={ref} href={href} onMouseEnter={update} onFocus={update} onBlur={() => setPosition((p) => ({ ...p, opacity: 0 }))} className="group flex items-center h-9 px-4 rounded-full text-sm font-medium text-current focus:outline-none" role="menuitem" > <span className="relative z-10 transition-colors group-hover:text-white group-focus:text-white opacity-80 group-hover:opacity-100"> {children} </span> </Link> </li> ); };
Usage
import Navbar from "@/components/zenblocks/navbar";
const navItems = [
{ label: "About", href: "/about" },
{ label: "Work", href: "/work" }
];
<Navbar items={navItems} />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items | NavbarItem[] | [] | Navigation link items |
| position | "fixed" | "relative" | "fixed" | Positioning mode |
| className | string | - | Additional CSS classes |
| logo | ReactNode | - | Custom logo component |
| brandText | ReactNode | "ZENBLOCKS" | Brand text to display |
| brandClassName | string | - | Classes for brand text styling |
NavbarItem
| Property | Type | Description |
|---|---|---|
| label | string | Link text |
| href | string | Navigation URL |
Accessibility
- Semantic
<nav>landmark - Keyboard navigation with Tab and Enter
- ARIA labels on mobile toggle button
- Focus indicators on all interactive elements
- Theme toggle includes proper ARIA attributes
Customization
Position
<Navbar position="relative" items={navItems} />Custom Brand
<Navbar
logo={<MyLogo className="w-6 h-6" />}
brandText="MY COMPANY"
brandClassName="text-blue-600 font-bold"
/>Custom Styling
<Navbar className="bg-white/90" items={navItems} />Motion Behavior
- Scroll effect: Background opacity and height change when scrolling >50px
- Logo animation: Blocks bounce vertically with staggered delays
- Tab indicator: Slides horizontally with spring physics (
stiffness: 380, damping: 32) - Mobile drawer: Items fade in with stagger on open
Performance Notes
- Scroll listener uses passive event option
- Logo animation respects
prefers-reduced-motion - Tab indicator position calculated via
getBoundingClientRect - Mobile/desktop variants use CSS media queries
Examples
Fixed Header
<Navbar
position="fixed"
items={navItems}
className="top-0 left-0 right-0"
/>Notes
- Default position is
fixedat top of viewport - Navbar is centered with max-width on desktop
- Mobile drawer appears below 640px breakpoint
- Requires
next-themesfor synchronized state - Uses Next.js Link component for navigation