A responsive site header that gains a border and shadow on scroll, can hide while scrolling down, and turns its links into a sheet menu on small screens.
Simple pricing for growing teams
Scroll this panel: the header gains a border and a soft shadow once content moves under it.
Starter
Everything you need to ship together, with support when you want it.
Team
Everything you need to ship together, with support when you want it.
Scale
Everything you need to ship together, with support when you want it.
"use client";
import * as React from "react";
import { Hexagon } from "lucide-react";
import { buttonVariants } from "@/components/ballmac/button";
import {
Navbar,
NavbarActions,
NavbarBrand,
NavbarLink,
NavbarLinks,
NavbarMobileLink,
NavbarMobileMenu,
} from "@/components/ballmac/navbar";
const links = [
["Product", "#product"],
["Pricing", "#pricing"],
["Docs", "#docs"],
["Changelog", "#changelog"],
] as const;
export default function NavbarDemo() {
const scroller = React.useRef<HTMLDivElement>(null);
return (
<div ref={scroller} className="h-80 w-full max-w-3xl overflow-auto rounded-xl border bg-background shadow-sm">
<Navbar scrollContainer={scroller} containerClassName="max-w-none">
<NavbarBrand href="#top">
<Hexagon aria-hidden="true" className="size-5 text-primary" /> Acme
</NavbarBrand>
<NavbarLinks className="ms-4">
{links.map(([label, href]) => (
<NavbarLink key={label} href={href} active={label === "Pricing"}>
{label}
</NavbarLink>
))}
</NavbarLinks>
<NavbarActions className="max-md:hidden">
<a href="#signin" className={buttonVariants({ variant: "ghost", size: "sm" })}>Sign in</a>
<a href="#start" className={buttonVariants({ size: "sm" })}>Get started</a>
</NavbarActions>
<NavbarMobileMenu label="Menu">
{links.map(([label, href]) => (
<NavbarMobileLink key={label} href={href} active={label === "Pricing"}>
{label}
</NavbarMobileLink>
))}
<a href="#start" className={buttonVariants({ className: "mt-3" })}>Get started</a>
</NavbarMobileMenu>
</Navbar>
<div className="grid gap-4 p-6">
<h2 className="text-2xl font-semibold tracking-tight">Simple pricing for growing teams</h2>
<p className="max-w-prose text-sm text-muted-foreground">Scroll this panel: the header gains a border and a soft shadow once content moves under it.</p>
{["Starter", "Team", "Scale"].map((plan) => (
<div key={plan} className="rounded-xl border p-5">
<p className="font-medium">{plan}</p>
<p className="text-sm text-muted-foreground">Everything you need to ship together, with support when you want it.</p>
</div>
))}
</div>
</div>
);
}Installation
$ pnpm dlx shadcn@latest add @ballmac/navbarInstall the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/sheet @ballmac/scroll @ballmac/i18nCopy the source into your project.
components/ballmac/navbar.tsx// Ballmac UI: Navbar. https://ui.ballmac.com/components/navbar "use client"; import * as React from "react"; import { Menu } from "lucide-react"; import { Sheet, SheetContent, SheetDescription, SheetTitle, SheetTrigger } from "@/components/ballmac/sheet"; import { useScrollDirection, useScrolled, type ScrollContainer } from "@/lib/ballmac/scroll"; import { cn } from "@/lib/utils"; import { useMessages } from "@/lib/ballmac/i18n"; type NavbarContextValue = { closeMobile: () => void; scrolled: boolean }; const NavbarContext = React.createContext<NavbarContextValue>({ closeMobile: () => {}, scrolled: false }); type NavbarProps = React.ComponentProps<"header"> & { /** Stick to the top of the page. */ sticky?: boolean; /** Slide out of view while scrolling down and back in while scrolling up. Stays visible while it holds focus. */ hideOnScroll?: boolean; /** When to draw the bottom border: `scrolled` only after the page moves, `always`, or `never`. */ border?: "scrolled" | "always" | "never"; /** Max width of the content row. */ containerClassName?: string; /** A scrollable element to watch instead of the page (for a header inside a panel or preview). */ scrollContainer?: ScrollContainer; }; /** A responsive site header. Compose NavbarBrand, NavbarLinks, NavbarActions and NavbarMobileMenu. */ function Navbar({ sticky = true, hideOnScroll = false, border = "scrolled", className, containerClassName, scrollContainer, children, ...props }: NavbarProps) { const scrolled = useScrolled(8, scrollContainer); const direction = useScrollDirection(12, scrollContainer); const hidden = hideOnScroll && scrolled && direction === "down"; const value = React.useMemo(() => ({ closeMobile: () => {}, scrolled }), [scrolled]); return ( <NavbarContext.Provider value={value}> <header data-slot="navbar" data-scrolled={scrolled || undefined} data-hidden={hidden || undefined} className={cn( "z-40 w-full border-b border-transparent bg-background/80 backdrop-blur-md transition-[transform,border-color,box-shadow] duration-300 motion-reduce:transition-none supports-[backdrop-filter]:bg-background/70", sticky && "sticky top-0", border === "always" && "border-border", border === "scrolled" && "data-[scrolled]:border-border data-[scrolled]:shadow-[0_1px_0_0_rgb(0_0_0/0.02),0_8px_24px_-16px_rgb(0_0_0/0.18)]", hidden && "-translate-y-full focus-within:translate-y-0", className, )} {...props} > <div className={cn("mx-auto flex h-14 w-full max-w-6xl items-center gap-4 px-4 sm:px-6", containerClassName)}> {children} </div> </header> </NavbarContext.Provider> ); } type NavbarBrandProps = React.ComponentProps<"a">; function NavbarBrand({ className, ...props }: NavbarBrandProps) { return ( <a data-slot="navbar-brand" className={cn( "-ms-1 flex shrink-0 items-center gap-2 rounded-md px-1 text-[15px] font-semibold tracking-tight outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50", className, )} {...props} /> ); } type NavbarLinksProps = React.ComponentProps<"nav"> & { /** Accessible name of the navigation landmark. */ label?: string; }; /** The primary links. Hidden below the `md` breakpoint; put the same links in NavbarMobileMenu. */ function NavbarLinks({ className, label, children, ...props }: NavbarLinksProps) { const msg = useMessages() label ??= msg("navbar.label", "Main") return ( <nav aria-label={label} data-slot="navbar-links" className={cn("hidden md:block", className)} {...props}> <ul className="flex items-center gap-1">{children}</ul> </nav> ); } type NavbarLinkProps = React.ComponentProps<"a"> & { /** Marks the link as the current page (`aria-current="page"`). */ active?: boolean; }; function NavbarLink({ active = false, className, children, ...props }: NavbarLinkProps) { return ( <li> <a data-slot="navbar-link" data-active={active || undefined} aria-current={active ? "page" : undefined} className={cn( "relative inline-flex h-9 items-center rounded-md px-3 text-sm font-medium text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[active]:text-foreground", "after:absolute after:inset-x-3 after:-bottom-[9px] after:h-0.5 after:scale-x-0 after:rounded-full after:bg-primary after:transition-transform after:duration-200 data-[active]:after:scale-x-100 motion-reduce:after:transition-none", className, )} {...props} > {children} </a> </li> ); } type NavbarActionsProps = React.ComponentProps<"div">; /** Buttons on the right: sign in, theme toggle, call to action. */ function NavbarActions({ className, ...props }: NavbarActionsProps) { return <div data-slot="navbar-actions" className={cn("ms-auto flex items-center gap-2", className)} {...props} />; } type NavbarMobileMenuProps = Omit<React.ComponentProps<typeof SheetContent>, "side"> & { /** Accessible name of the menu button and the panel. */ label?: string; }; /** A menu button (shown below `md`) that opens the links in a sheet. Links inside close it when chosen. */ function NavbarMobileMenu({ label, className, children, ...props }: NavbarMobileMenuProps) { const msg = useMessages() label ??= msg("navbar.label2", "Menu") const [open, setOpen] = React.useState(false); const parent = React.useContext(NavbarContext); const value = React.useMemo(() => ({ ...parent, closeMobile: () => setOpen(false) }), [parent]); return ( <Sheet open={open} onOpenChange={setOpen}> <SheetTrigger data-slot="navbar-mobile-trigger" aria-label={label} className="ms-auto inline-flex size-9 items-center justify-center rounded-md outline-none transition-colors hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 md:hidden" > <Menu aria-hidden="true" className="size-5" /> </SheetTrigger> <SheetContent side="end" className={cn("w-72 sm:w-80", className)} {...props}> <SheetTitle className="px-5 pt-5 text-base font-semibold">{label}</SheetTitle> <SheetDescription className="sr-only">{msg("navbar.siteNavigation", "Site navigation")}</SheetDescription> <NavbarContext.Provider value={value}> <nav aria-label={label} className="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto px-3 pb-5"> {children} </nav> </NavbarContext.Provider> </SheetContent> </Sheet> ); } type NavbarMobileLinkProps = React.ComponentProps<"a"> & { active?: boolean }; /** A full-width link for NavbarMobileMenu that closes the menu on click. */ function NavbarMobileLink({ active = false, className, onClick, ...props }: NavbarMobileLinkProps) { const { closeMobile } = React.useContext(NavbarContext); return ( <a data-slot="navbar-mobile-link" data-active={active || undefined} aria-current={active ? "page" : undefined} onClick={(event) => { onClick?.(event); closeMobile(); }} className={cn( "flex h-11 items-center rounded-lg px-3 text-[15px] font-medium outline-none transition-colors hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[active]:bg-accent data-[active]:text-accent-foreground", className, )} {...props} /> ); } export { Navbar, NavbarBrand, NavbarLinks, NavbarLink, NavbarActions, NavbarMobileMenu, NavbarMobileLink, type NavbarProps, type NavbarBrandProps, type NavbarLinksProps, type NavbarLinkProps, type NavbarActionsProps, type NavbarMobileMenuProps, type NavbarMobileLinkProps, };Update the import paths to match your project setup.
Usage
import { Navbar, NavbarBrand, NavbarLinks, NavbarLink, NavbarActions, NavbarMobileMenu, NavbarMobileLink } from "@/components/ballmac/navbar"The full example is in the Code tab above.
Examples
Hide on scroll
Hides while you scroll down, returns when you scroll up.
Paragraph 1: long pages keep the content in view and bring navigation back on demand.
Paragraph 2: long pages keep the content in view and bring navigation back on demand.
Paragraph 3: long pages keep the content in view and bring navigation back on demand.
Paragraph 4: long pages keep the content in view and bring navigation back on demand.
Paragraph 5: long pages keep the content in view and bring navigation back on demand.
Paragraph 6: long pages keep the content in view and bring navigation back on demand.
Paragraph 7: long pages keep the content in view and bring navigation back on demand.
Paragraph 8: long pages keep the content in view and bring navigation back on demand.
Paragraph 9: long pages keep the content in view and bring navigation back on demand.
Paragraph 10: long pages keep the content in view and bring navigation back on demand.
"use client";
import * as React from "react";
import { Hexagon } from "lucide-react";
import { Navbar, NavbarBrand, NavbarLink, NavbarLinks } from "@/components/ballmac/navbar";
export default function NavbarStates() {
const scroller = React.useRef<HTMLDivElement>(null);
return (
<div ref={scroller} className="h-64 w-full max-w-md overflow-auto rounded-xl border bg-background">
<Navbar hideOnScroll border="always" scrollContainer={scroller} containerClassName="max-w-none">
<NavbarBrand href="#top">
<Hexagon aria-hidden="true" className="size-5 text-primary" /> Acme
</NavbarBrand>
<NavbarLinks className="ms-auto">
<NavbarLink href="#a" active>Overview</NavbarLink>
<NavbarLink href="#b">Guides</NavbarLink>
</NavbarLinks>
</Navbar>
<div className="grid gap-3 p-5 text-sm text-muted-foreground">
<p className="font-medium text-foreground">Hides while you scroll down, returns when you scroll up.</p>
{Array.from({ length: 10 }, (_, i) => (
<p key={i} className="rounded-lg border p-3">Paragraph {i + 1}: long pages keep the content in view and bring navigation back on demand.</p>
))}
</div>
</div>
);
}API reference
<Navbar>
| Prop | Type | Default |
|---|---|---|
stickyStick to the top of the page. | boolean | true |
hideOnScrollSlide out of view while scrolling down and back in while scrolling up. Stays visible while it holds focus. | boolean | false |
borderWhen to draw the bottom border: `scrolled` only after the page moves, `always`, or `never`. | "scrolled" | "always" | "never" | "scrolled" |
containerClassNameMax width of the content row. | string | — |
scrollContainerA scrollable element to watch instead of the page (for a header inside a panel or preview). | ScrollContainer | — |
<NavbarLinks>
| Prop | Type | Default |
|---|---|---|
labelAccessible name of the navigation landmark. | string | — |
<NavbarLink>
| Prop | Type | Default |
|---|---|---|
activeMarks the link as the current page (`aria-current="page"`). | boolean | false |
<NavbarMobileMenu>
| Prop | Type | Default |
|---|---|---|
labelAccessible name of the menu button and the panel. | string | — |
<NavbarMobileLink>
| Prop | Type | Default |
|---|---|---|
active | boolean | false |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab | Moves through brand, links, actions and the menu button |
| Enter / Space | Opens the mobile menu; Escape closes it and returns focus |
| Screen readers | Links are in a labelled navigation landmark; the current page has aria-current |
Use with AI
Compose Navbar with NavbarBrand, NavbarLinks/NavbarLink (active sets aria-current), NavbarActions and NavbarMobileMenu with NavbarMobileLink. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Navbar (@ballmac/navbar) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Marketing and docs site headers
- Headers that should get out of the way on long pages
Not for
- Application chrome with a sidebar; use app-shell
- Wide dropdown panels; add mega-menu
Registry JSON: https://ui.ballmac.com/r/navbar.json
Credits
Free to use in personal and commercial projects.
- npm
- lucide-react
Pairs well with
Mega Menu
A data-driven header menu with wide panels of grouped, described links, an optional featured card, and an accordion list for small screens.
Button
A button with six variants, three sizes, a pill shape and a built-in loading state. buttonVariants() styles links the same way.
Sheet
A modal panel that slides from any edge, with header, scrollable body and footer parts, a grab handle on bottom sheets, and safe-area padding.
Animated Tabs
Radix Tabs with a pill or underline indicator that slides between triggers on a spring, and panels that fade in. Keeps full keyboard support and ARIA; controlled or uncontrolled.