Site header built on Navigation Menu in left, centered-links and centered-logo layouts. Container queries hide links on narrow widths and wrap actions; parts include logo, links and actions.
import type { ComponentProps } from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { NavigationMenu as NavigationMenuPrimitive } from "@base-ui/react/navigation-menu"
import { useRender } from "@base-ui/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import { NavigationMenu } from "@/components/ui/navigation-menu"
const navbarVariants = cva(
"grid min-h-21 grid-cols-[auto_1fr] items-center gap-x-4 gap-y-3 py-3 @max-xs/navbar:grid-cols-1",
{
variants: {
/**
* How the bar arranges its parts once it is 64rem wide: `left` (the
* default) puts the links after the logo, `centered-links` centers the
* links, and `centered-logo` centers the logo with the links on its
* left. A narrower bar hides the links whatever the variant.
*/
variant: {
left: "@5xl/navbar:grid-cols-[auto_1fr_auto] @5xl/navbar:gap-x-12",
"centered-links": "@5xl/navbar:grid-cols-[1fr_auto_1fr]",
"centered-logo": "@5xl/navbar:grid-cols-[1fr_auto_1fr]",
},
},
defaultVariants: { variant: "left" },
}
)
/**
* The site header bar, as a `header`. Holds `NavbarLogo`, `NavbarLinks` and
* `NavbarActions`, and sets them in a centered `NavbarContainer`.
*
* Its layout follows its own width. Below 64rem it shows the logo and the
* actions and hides the links, so give narrow screens their own menu, for
* example in `NavbarActions`. From 64rem `variant` arranges all three: `left`
* (the default) puts the links after the logo, `centered-links` centers the
* links, and `centered-logo` centers the logo with the links on its left.
* Below 20rem the actions drop under the logo. `className` lands on the
* outer `header`.
*/
function Navbar({
className,
variant = "left",
children,
...props
}: ComponentProps<"header"> & VariantProps<typeof navbarVariants>) {
return (
<header
data-slot="navbar"
data-variant={variant}
className={cn(
"group/navbar @container/navbar relative isolate bg-background text-foreground",
className
)}
{...props}
>
<NavbarContainer className={navbarVariants({ variant })}>
{children}
</NavbarContainer>
</header>
)
}
/**
* A layer for art or a fill behind the bar's content. Place it as a child of
* `Navbar`: it covers the whole bar and is hidden from assistive tech and the
* pointer.
*/
function NavbarBackground({ className, ...props }: ComponentProps<"div">) {
return (
<div
data-slot="navbar-background"
aria-hidden="true"
className={cn("pointer-events-none absolute inset-0 -z-10", className)}
{...props}
/>
)
}
/**
* The centered column that holds the bar's content, with side padding. Its
* width steps with the viewport: 42rem, 48rem from `md`, and 80rem from `lg`.
* `Navbar` already renders one around its children.
*/
function NavbarContainer({ className, ...props }: ComponentProps<"div">) {
return (
<div
data-slot="navbar-container"
className={cn(
"mx-auto w-full max-w-2xl px-6 md:max-w-3xl lg:max-w-7xl lg:px-10",
className
)}
{...props}
/>
)
}
/**
* The brand link at the start of the bar, an `a` by default. Pass
* `render={<Link href="/" />}` to use your router's link. In the
* `centered-logo` variant it moves to the center from 64rem.
*/
function NavbarLogo({
className,
render,
...props
}: useRender.ComponentProps<"a">) {
return useRender({
defaultTagName: "a",
props: mergeProps<"a">(
{
className: cn(
"col-start-1 row-start-1 inline-flex w-fit min-w-0 items-center gap-2 rounded-sm text-xl font-semibold focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-ring @5xl/navbar:group-data-[variant=centered-logo]/navbar:col-start-2 @5xl/navbar:group-data-[variant=centered-logo]/navbar:justify-self-center",
className
),
},
props
),
render,
state: { slot: "navbar-logo" },
})
}
/**
* The main links, built on `NavigationMenu`. Put a `NavigationMenuList` of
* `NavigationMenuItem` entries inside, each holding a `NavbarLink` or a menu
* trigger. It is hidden until the navbar is 64rem wide.
*
* `className` takes a string only: it is merged with `cn`, so Base UI's
* function form for `className` is not supported here.
*/
function NavbarLinks({
className,
...props
}: Omit<ComponentProps<typeof NavigationMenu>, "className"> & {
/**
* Extra classes for the `NavigationMenu` root, as a string only: Base UI's
* function form is not accepted.
*/
className?: string
}) {
return (
<NavigationMenu
data-slot="navbar-links"
className={cn(
"col-start-2 row-start-1 hidden min-w-0 @5xl/navbar:flex @5xl/navbar:group-data-[variant=centered-logo]/navbar:col-start-1",
className
)}
{...props}
/>
)
}
/**
* A text link for `NavbarLinks`, underlined on hover and while `active` is
* set. It is the plain Base UI navigation menu link with its own style, not
* the filled menu link. Pass `render={<Link href="/pricing" />}` for a router
* link.
*
* `className` takes a string only.
*/
function NavbarLink({
className,
...props
}: Omit<NavigationMenuPrimitive.Link.Props, "className"> & {
/**
* Extra classes for the link, as a string only: Base UI's function form
* is not accepted.
*/
className?: string
}) {
return (
<NavigationMenuPrimitive.Link
data-slot="navbar-link"
className={cn(
"inline-flex h-9 w-max items-center justify-center gap-1.5 rounded-sm text-sm font-medium underline-offset-4 transition-colors outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/30 focus-visible:outline-1 data-active:underline",
className
)}
{...props}
/>
)
}
/**
* The buttons at the end of the bar, such as sign-in or a menu for narrow
* screens, right-aligned with 16px gaps. Below 20rem they drop to their own
* row under the logo and wrap.
*/
function NavbarActions({ className, ...props }: ComponentProps<"div">) {
return (
<div
data-slot="navbar-actions"
className={cn(
"col-start-2 row-start-1 flex min-w-0 items-center justify-end gap-4 justify-self-end @max-xs/navbar:col-start-1 @max-xs/navbar:row-start-2 @max-xs/navbar:flex-wrap @max-xs/navbar:justify-start @max-xs/navbar:justify-self-start @5xl/navbar:col-start-3",
className
)}
{...props}
/>
)
}
export {
Navbar,
NavbarBackground,
NavbarContainer,
NavbarLogo,
NavbarLinks,
NavbarLink,
NavbarActions,
navbarVariants,
}
pnpm dlx shadcn add @01orbital/navbarnpx shadcn add @01orbital/navbaryarn shadcn add @01orbital/navbarbunx --bun shadcn add @01orbital/navbarFree under the MIT license. Add the @01orbital registry to components.json once (install guide), then run the command.
It lands at components/site-nav/navbar.tsx, as source you own and edit.
The example also uses button, separator and sheet. Install them too:
pnpm dlx shadcn add @01orbital/button @01orbital/separator @01orbital/sheetnpx shadcn add @01orbital/button @01orbital/separator @01orbital/sheetyarn shadcn add @01orbital/button @01orbital/separator @01orbital/sheetbunx --bun shadcn add @01orbital/button @01orbital/separator @01orbital/sheet"use client"
import { useState } from "react"
import { CircleIcon, ListIcon } from "@phosphor-icons/react"
import { cn } from "cn"
import {
Navbar,
NavbarActions,
NavbarLink,
NavbarLinks,
NavbarLogo,
} from "@/components/site-nav/navbar"
import { Button, buttonVariants } from "@/components/ui/button"
import {
NavigationMenuItem,
NavigationMenuList,
} from "@/components/ui/navigation-menu"
import { Separator } from "@/components/ui/separator"
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet"
const COPY = {
brand: "Orbcore",
home: "Orbcore home",
navigation: "Main navigation",
mobileNavigation: "Mobile navigation",
menu: "Explore Orbcore",
openMenu: "Open menu",
login: "Log in",
action: "Get started",
links: [
{ label: "Product", href: "#product" },
{ label: "Solutions", href: "#solutions" },
{ label: "Resources", href: "#resources" },
{ label: "Pricing", href: "#pricing" },
],
layouts: [
{ variant: "left", label: "Left aligned links" },
{ variant: "centered-links", label: "Centered links" },
{ variant: "centered-logo", label: "Centered logo" },
],
} as const
export default function NavbarDemo() {
const [openVariant, setOpenVariant] = useState<
(typeof COPY.layouts)[number]["variant"] | null
>(null)
return (
<div className="grid w-full min-w-0 place-items-center gap-8 p-6 py-8">
{COPY.layouts.map(({ variant, label }, index) => (
<section key={variant} className="flex flex-col gap-3">
<div className="mx-auto w-full max-w-2xl px-6 md:max-w-3xl lg:max-w-7xl lg:px-10">
<h2 className="text-sm font-medium text-muted-foreground">
{label}
</h2>
</div>
<Navbar variant={variant}>
<NavbarLogo href="#" aria-label={COPY.home}>
<CircleIcon
weight="light"
aria-hidden="true"
className="size-5 shrink-0"
/>
{COPY.brand}
</NavbarLogo>
<NavbarLinks aria-label={COPY.navigation}>
<NavigationMenuList className="gap-8">
{COPY.links.map(({ label, href }) => (
<NavigationMenuItem key={label}>
<NavbarLink render={<a href={href} />}>{label}</NavbarLink>
</NavigationMenuItem>
))}
</NavigationMenuList>
</NavbarLinks>
<NavbarActions>
<a
href="#"
data-slot="button"
className={cn(
buttonVariants({ variant: "ghost" }),
"hidden @5xl/navbar:inline-flex"
)}
>
{COPY.login}
</a>
<a href="#" data-slot="button" className={cn(buttonVariants())}>
{COPY.action}
</a>
<Sheet
open={openVariant === variant}
onOpenChange={(open) => setOpenVariant(open ? variant : null)}
>
<SheetTrigger
render={
<Button
variant="ghost"
size="icon"
className="@5xl/navbar:hidden"
aria-label={COPY.openMenu}
/>
}
>
<ListIcon aria-hidden="true" />
</SheetTrigger>
<SheetContent className="overflow-y-auto">
<SheetHeader>
<SheetTitle>{COPY.menu}</SheetTitle>
</SheetHeader>
<nav
aria-label={COPY.mobileNavigation}
className="flex flex-col gap-2 px-6"
>
{COPY.links.map(({ label, href }) => (
<a
key={label}
href={href}
onClick={() => setOpenVariant(null)}
className="flex min-h-11 items-center rounded-lg text-xl font-medium focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
>
{label}
</a>
))}
</nav>
<SheetFooter>
<a
href="#"
onClick={() => setOpenVariant(null)}
data-slot="button"
className={cn(buttonVariants({ variant: "outline" }))}
>
{COPY.login}
</a>
<a
href="#"
onClick={() => setOpenVariant(null)}
data-slot="button"
className={cn(buttonVariants())}
>
{COPY.action}
</a>
</SheetFooter>
</SheetContent>
</Sheet>
</NavbarActions>
</Navbar>
{index < COPY.layouts.length - 1 && <Separator />}
</section>
))}
</div>
)
}Navbar is built from 7 parts. In the example they nest like this:
Navbar
├─ NavbarLogo
├─ NavbarLinks
│ └─ NavbarLink
└─ NavbarActionsNavbar: The site header bar, as a header. Holds NavbarLogo, NavbarLinks and NavbarActions, and sets them in a centered NavbarContainer.NavbarLogo: The brand link at the start of the bar, an a by default. Pass render={<Link href="/" />} to use your router's link. In the centered-logo variant it moves to the center from 64rem.NavbarLinks: The main links, built on NavigationMenu. Put a NavigationMenuList of NavigationMenuItem entries inside, each holding a NavbarLink or a menu trigger. It is hidden until the navbar is 64rem wide.NavbarLink: A text link for NavbarLinks, underlined on hover and while active is set. It is the plain Base UI navigation menu link with its own style, not the filled menu link. Pass render={<Link href="/pricing" />} for a router link.NavbarActions: The buttons at the end of the bar, such as sign-in or a menu for narrow screens, right-aligned with 16px gaps. Below 20rem they drop to their own row under the logo and wrap.NavbarBackground: A layer for art or a fill behind the bar's content. Place it as a child of Navbar: it covers the whole bar and is hidden from assistive tech and the pointer.NavbarContainer: The centered column that holds the bar's content, with side padding. Its width steps with the viewport: 42rem, 48rem from md, and 80rem from lg. Navbar already renders one around its children.The site header bar, as a header. Holds NavbarLogo, NavbarLinks and NavbarActions, and sets them in a centered NavbarContainer.
Its layout follows its own width. Below 64rem it shows the logo and the actions and hides the links, so give narrow screens their own menu, for example in NavbarActions. From 64rem variant arranges all three: left (the default) puts the links after the logo, centered-links centers the links, and centered-logo centers the logo with the links on its left. Below 20rem the actions drop under the logo. className lands on the outer header.
Prop
Type
It also accepts the HTML attributes of the element it renders.
The brand link at the start of the bar, an a by default. Pass render={<Link href="/" />} to use your router's link. In the centered-logo variant it moves to the center from 64rem.
It accepts the props of the Base UI part it wraps, and the HTML attributes of the element it renders.
The main links, built on NavigationMenu. Put a NavigationMenuList of NavigationMenuItem entries inside, each holding a NavbarLink or a menu trigger. It is hidden until the navbar is 64rem wide.
className takes a string only: it is merged with cn, so Base UI's function form for className is not supported here.
Prop
Type
It also accepts the props of the Base UI part it wraps, and the HTML attributes of the element it renders.
A text link for NavbarLinks, underlined on hover and while active is set. It is the plain Base UI navigation menu link with its own style, not the filled menu link. Pass render={<Link href="/pricing" />} for a router link.
className takes a string only.
Prop
Type
It also accepts the props of the Base UI part it wraps, and the HTML attributes of the element it renders.
The buttons at the end of the bar, such as sign-in or a menu for narrow screens, right-aligned with 16px gaps. Below 20rem they drop to their own row under the logo and wrap.
It accepts the HTML attributes of the element it renders.
A layer for art or a fill behind the bar's content. Place it as a child of Navbar: it covers the whole bar and is hidden from assistive tech and the pointer.
It accepts the HTML attributes of the element it renders.
The centered column that holds the bar's content, with side padding. Its width steps with the viewport: 42rem, 48rem from md, and 80rem from lg. Navbar already renders one around its children.
It accepts the HTML attributes of the element it renders.
npm: @base-ui/react, class-variance-authority, cn. The CLI installs them for you.
Registry pieces it installs with it:
navigation-menuVoice Orb
Painted CSS orb for voice agents, a cobalt tide rising into scarlet that breathes faster across idle, listening and speaking states. Decorative and reduced-motion aware.
Metric Card
Compact KPI card: a metric name, a large tabular value, an optional outline badge with a trend-up or trend-down icon and screen-reader label, and an optional description line.