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

Install via the ZenBlocks CLI to automatically handle dependencies:

npx shadcn@latest add https://zenblocks-three.vercel.app/r/navbar.json

Note: The CLI does not set up the global ThemeProvider. Please see the Configuration guide to set up dark mode support.

Method 2: Manual

  1. Install Dependencies

    npm install framer-motion lucide-react next-themes
  2. Setup Theme Provider

    This component uses next-themes. Ensure your app is wrapped in a ThemeProvider as described in the Global Installation guide.

  3. 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

PropTypeDefaultDescription
itemsNavbarItem[][]Navigation link items
position"fixed" | "relative""fixed"Positioning mode
classNamestring-Additional CSS classes
logoReactNode-Custom logo component
brandTextReactNode"ZENBLOCKS"Brand text to display
brandClassNamestring-Classes for brand text styling
PropertyTypeDescription
labelstringLink text
hrefstringNavigation 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 fixed at top of viewport
  • Navbar is centered with max-width on desktop
  • Mobile drawer appears below 640px breakpoint
  • Requires next-themes for synchronized state
  • Uses Next.js Link component for navigation