Navbar
Self-contained navbar layouts with scroll behavior, variant backgrounds, and mobile menu integration. Pick one and edit directly.
Classic
Floating pill with scroll-hide behavior. Logo left, links and CTA right. Transitions from variant background to blurred on scroll.
API Reference
| Prop | Type | Default |
|---|---|---|
| variant | "solid" | "outlined" | "ghost" | "minimal" | "solid" |
| theme | Theme | — |
Basic Usage
import { Navbar } from "@/extensions/layout";
<Navbar />Source
src/extensions/layout/Navbar.tsx
"use client";
import React, { useState, useEffect } from "react";
import { usePathname } from "next/navigation";
import {
NavMenu,
NavMenuLink,
Button,
IconButton,
Link,
Logo,
Container,
} from "@/ui";
import { MenuIcon } from "@/ui/icons";
import { NAV_LINKS, NAV_CTA } from "@/constants";
import type { Theme } from "@/types";
import { cn } from "@/utils";
import { MobileMenu } from "./MobileMenu";
interface NavbarProps {
variant?: "solid" | "outlined" | "ghost" | "minimal";
theme?: Theme;
}
function getAtTopBg(variant: NavbarProps["variant"]) {
switch (variant) {
case "outlined":
return "border border-root-contrast/20 bg-transparent backdrop-blur-sm";
case "ghost":
return "border border-transparent bg-divider backdrop-blur-sm";
case "minimal":
return "border border-transparent bg-transparent";
case "solid":
default:
return "border border-divider bg-root/90 shadow-sm backdrop-blur-md";
}
}
const SCROLLED_BG =
"border border-transparent bg-root/60 backdrop-blur-lg";
export function Navbar({ variant = "solid", theme }: NavbarProps) {
const pathname = usePathname();
const [mobileOpen, setMobileOpen] = useState(false);
const [visible, setVisible] = useState(true);
const [atTop, setAtTop] = useState(true);
useEffect(() => {
let lastY = window.scrollY;
const onScroll = () => {
const currentY = window.scrollY;
setAtTop(currentY < 78);
setVisible(currentY < 10 || currentY < lastY);
lastY = currentY;
};
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
return (
<>
<nav
aria-label="Main navigation"
className={cn(
"fixed inset-x-0 top-3 z-40 transition-all duration-300",
visible
? "pointer-events-auto translate-y-0 opacity-100"
: "pointer-events-none -translate-y-2 opacity-0",
atTop && theme,
)}
>
<Container>
<div
className={cn(
"rounded-2xl px-4 py-3 transition-all duration-300",
atTop ? getAtTopBg(variant) : SCROLLED_BG,
)}
>
<div className="flex items-center justify-between">
<Link href="/">
<Logo />
</Link>
<div className="flex items-center gap-8">
<NavMenu spacing="lg">
{NAV_LINKS.map((link) => {
const exactMatch = pathname === link.href;
const otherExactMatch = NAV_LINKS.some(
(other) =>
other.href !== link.href && pathname === other.href,
);
const href = link.href as string;
const prefixMatch =
!otherExactMatch &&
pathname.startsWith(href) &&
href !== "/";
const isActive = exactMatch || prefixMatch;
return (
<NavMenuLink
key={link.href}
href={link.href}
isActive={isActive}
variant="underline"
>
{link.label}
</NavMenuLink>
);
})}
</NavMenu>
<div className="hidden md:inline-flex">
<Button
href={NAV_CTA.href}
variant="solid"
color="root"
size="sm"
>
{NAV_CTA.label}
</Button>
</div>
<div className="md:hidden">
<IconButton
variant="ghost"
color="root"
size="sm"
onClick={() => setMobileOpen(true)}
aria-label="Open menu"
>
<MenuIcon />
</IconButton>
</div>
</div>
</div>
</div>
</Container>
</nav>
<MobileMenu open={mobileOpen} onClose={() => setMobileOpen(false)} />
</>
);
}Centered
Floating pill with scroll-hide behavior. Logo left, links absolutely centered, CTA right. Same variant system as Navbar.
API Reference
| Prop | Type | Default |
|---|---|---|
| variant | "solid" | "outlined" | "ghost" | "minimal" | "solid" |
| theme | Theme | — |
Basic Usage
import { NavbarCentered } from "@/extensions/layout";
<NavbarCentered />Source
src/extensions/layout/NavbarCentered.tsx
"use client";
import React, { useState, useEffect } from "react";
import { usePathname } from "next/navigation";
import {
NavMenu,
NavMenuLink,
Button,
IconButton,
Link,
Logo,
Container,
} from "@/ui";
import { MenuIcon } from "@/ui/icons";
import { NAV_LINKS, NAV_CTA } from "@/constants";
import type { Theme } from "@/types";
import { cn } from "@/utils";
import { MobileMenu } from "./MobileMenu";
interface NavbarCenteredProps {
variant?: "solid" | "outlined" | "ghost" | "minimal";
theme?: Theme;
}
function getAtTopBg(variant: NavbarCenteredProps["variant"]) {
switch (variant) {
case "outlined":
return "border border-root-contrast/20 bg-transparent backdrop-blur-sm";
case "ghost":
return "border border-transparent bg-divider backdrop-blur-sm";
case "minimal":
return "border border-transparent bg-transparent";
case "solid":
default:
return "border border-divider bg-root/90 shadow-sm backdrop-blur-md";
}
}
const SCROLLED_BG =
"border border-transparent bg-root/60 backdrop-blur-lg";
export function NavbarCentered({
variant = "solid",
theme,
}: NavbarCenteredProps) {
const pathname = usePathname();
const [mobileOpen, setMobileOpen] = useState(false);
const [visible, setVisible] = useState(true);
const [atTop, setAtTop] = useState(true);
useEffect(() => {
let lastY = window.scrollY;
const onScroll = () => {
const currentY = window.scrollY;
setAtTop(currentY < 78);
setVisible(currentY < 10 || currentY < lastY);
lastY = currentY;
};
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
return (
<>
<nav
aria-label="Main navigation"
className={cn(
"fixed inset-x-0 top-3 z-40 transition-all duration-300",
visible
? "pointer-events-auto translate-y-0 opacity-100"
: "pointer-events-none -translate-y-2 opacity-0",
atTop && theme,
)}
>
<Container>
<div
className={cn(
"rounded-2xl px-4 py-3 transition-all duration-300",
atTop ? getAtTopBg(variant) : SCROLLED_BG,
)}
>
<div className="relative flex items-center justify-between">
<Link href="/">
<Logo />
</Link>
<NavMenu
className="absolute left-1/2 -translate-x-1/2"
spacing="lg"
>
{NAV_LINKS.map((link) => {
const exactMatch = pathname === link.href;
const otherExactMatch = NAV_LINKS.some(
(other) =>
other.href !== link.href && pathname === other.href,
);
const href = link.href as string;
const prefixMatch =
!otherExactMatch &&
pathname.startsWith(href) &&
href !== "/";
const isActive = exactMatch || prefixMatch;
return (
<NavMenuLink
key={link.href}
href={link.href}
isActive={isActive}
variant="underline"
>
{link.label}
</NavMenuLink>
);
})}
</NavMenu>
<div className="hidden md:inline-flex">
<Button
href={NAV_CTA.href}
variant="solid"
color="root"
size="sm"
>
{NAV_CTA.label}
</Button>
</div>
<div className="md:hidden">
<IconButton
variant="ghost"
color="root"
size="sm"
onClick={() => setMobileOpen(true)}
aria-label="Open menu"
>
<MenuIcon />
</IconButton>
</div>
</div>
</div>
</Container>
</nav>
<MobileMenu open={mobileOpen} onClose={() => setMobileOpen(false)} />
</>
);
}Static
Full-width bar with no scroll behavior. Fixed or relative positioning via prop. Best for apps and dashboards.
API Reference
| Prop | Type | Default |
|---|---|---|
| variant | "solid" | "outlined" | "ghost" | "minimal" | "solid" |
| position | "fixed" | "relative" | "fixed" |
| theme | Theme | — |
Basic Usage
import { NavbarStatic } from "@/extensions/layout";
<NavbarStatic />Source
src/extensions/layout/NavbarStatic.tsx
"use client";
import { useState } from "react";
import { usePathname } from "next/navigation";
import {
NavMenu,
NavMenuLink,
Button,
IconButton,
Link,
Logo,
Container,
} from "@/ui";
import { MenuIcon } from "@/ui/icons";
import { NAV_LINKS, NAV_CTA } from "@/constants";
import type { Theme } from "@/types";
import { cn } from "@/utils";
import { MobileMenu } from "./MobileMenu";
interface NavbarStaticProps {
variant?: "solid" | "outlined" | "ghost" | "minimal";
position?: "fixed" | "relative";
theme?: Theme;
}
function getBg(variant: NavbarStaticProps["variant"]) {
switch (variant) {
case "outlined":
return "border-b border-root-contrast/20 bg-transparent backdrop-blur-sm";
case "ghost":
return "border-b border-transparent bg-divider backdrop-blur-sm";
case "minimal":
return "border-b border-transparent bg-transparent";
case "solid":
default:
return "border-b border-divider bg-root/90 shadow-sm backdrop-blur-md";
}
}
export function NavbarStatic({
variant = "solid",
position = "fixed",
theme,
}: NavbarStaticProps) {
const pathname = usePathname();
const [mobileOpen, setMobileOpen] = useState(false);
const isFixed = position === "fixed";
return (
<>
<nav
aria-label="Main navigation"
className={cn(
"inset-x-0 top-0 z-40",
isFixed ? "fixed" : "relative",
getBg(variant),
theme,
)}
>
<Container className="py-3">
<div className="flex items-center justify-between">
<Link href="/">
<Logo />
</Link>
<div className="flex items-center gap-8">
<NavMenu spacing="lg">
{NAV_LINKS.map((link) => {
const exactMatch = pathname === link.href;
const otherExactMatch = NAV_LINKS.some(
(other) =>
other.href !== link.href && pathname === other.href,
);
const href = link.href as string;
const prefixMatch =
!otherExactMatch &&
pathname.startsWith(href) &&
href !== "/";
const isActive = exactMatch || prefixMatch;
return (
<NavMenuLink
key={link.href}
href={link.href}
isActive={isActive}
variant="underline"
>
{link.label}
</NavMenuLink>
);
})}
</NavMenu>
<div className="hidden md:inline-flex">
<Button
href={NAV_CTA.href}
variant="solid"
color="root"
size="sm"
>
{NAV_CTA.label}
</Button>
</div>
<div className="md:hidden">
<IconButton
variant="ghost"
color="root"
size="sm"
onClick={() => setMobileOpen(true)}
aria-label="Open menu"
>
<MenuIcon />
</IconButton>
</div>
</div>
</div>
</Container>
</nav>
<MobileMenu open={mobileOpen} onClose={() => setMobileOpen(false)} />
</>
);
}- All three navbars pull links from
NAV_LINKSand the CTA fromNAV_CTAin@/constants. NavbarandNavbarCenteredare fixed floating pills that hide on scroll-down and reappear on scroll-up.NavbarStatichas no scroll behavior — useposition="relative"for an in-flow bar that needs nopt-navbarclearance.- Pass
variant="minimal"andtheme="dark"to float transparently over a dark hero section. - Each navbar imports
MobileMenudirectly — swap it forMobileMenuLayeredby editing the navbar file.