All components
NucleusNavbar
NavigationNewA responsive navbar — horizontal links on desktop, hamburger + slide-out panel on mobile, with logo and CTA slots.
Preview
orbit
Installation
- 1shadcn/ui initialized (`npx shadcn@latest init`)
- 2Tailwind CSS v4 configured with the nucleusUI theme tokens
npm install lucide-react clsx tailwind-mergePrerequisite — install this first
The usage example depends on components/nucleus/nucleus-button.tsx. Copy it into your project before copying the main component.
components/nucleus/nucleus-button.tsx
"use client";
import * as React from "react";
import { Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
type Variant = "primary" | "secondary" | "outline" | "ghost" | "destructive" | "link";
type Size = "sm" | "md" | "lg" | "icon";
export interface NucleusButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** Visual style of the button. */
variant?: Variant;
/** Control the height / padding. */
size?: Size;
/** Shows a spinner and disables interaction when true. */
loading?: boolean;
/** Optional icon rendered before the label. */
icon?: React.ReactNode;
/** Optional icon rendered after the label. */
iconEnd?: React.ReactNode;
}
const variantClasses: Record<Variant, string> = {
primary:
"bg-primary text-primary-foreground shadow-sm hover:bg-primary/90 active:bg-primary/95",
secondary:
"bg-secondary text-secondary-foreground border border-border/60 hover:bg-secondary/70",
outline:
"border border-border bg-transparent hover:bg-muted hover:border-primary/40",
ghost: "hover:bg-muted",
destructive: "bg-destructive text-white shadow-sm hover:bg-destructive/90",
link: "text-primary underline-offset-4 hover:underline",
};
const sizeClasses: Record<Size, string> = {
sm: "h-8 gap-1.5 rounded-lg px-3 text-xs",
md: "h-10 gap-2 rounded-xl px-4 text-sm",
lg: "h-12 gap-2 rounded-xl px-6 text-base",
icon: "size-10 rounded-xl",
};
export const NucleusButton = React.forwardRef<HTMLButtonElement, NucleusButtonProps>(
function NucleusButton(
{
className,
variant = "primary",
size = "md",
loading = false,
icon,
iconEnd,
children,
disabled,
type = "button",
...props
},
ref
) {
return (
<button
ref={ref}
type={type}
disabled={disabled || loading}
data-slot="nucleus-button"
data-variant={variant}
className={cn(
"inline-flex shrink-0 cursor-pointer items-center justify-center font-medium whitespace-nowrap transition-all duration-150 select-none",
"focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none",
"disabled:pointer-events-none disabled:opacity-55 active:translate-y-px",
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variantClasses[variant],
sizeClasses[size],
className
)}
{...props}
>
{loading ? (
<Loader2 className="animate-spin" aria-hidden />
) : (
icon
)}
{children}
{!loading && iconEnd}
</button>
);
}
);
Add to your globals.css
The nucleusUI accent lives in these CSS variables — without them the component falls back to the default shadcn zinc theme.
app/globals.css
:root {
--primary: oklch(0.54 0.24 291);
--primary-foreground: oklch(0.985 0 0);
--ring: oklch(0.54 0.24 291);
--accent: oklch(0.96 0.03 291);
--accent-foreground: oklch(0.4 0.19 291);
}
.dark {
--primary: oklch(0.68 0.21 291);
--primary-foreground: oklch(0.145 0 0);
--ring: oklch(0.68 0.21 291);
--accent: oklch(0.26 0.06 291);
--accent-foreground: oklch(0.87 0.09 291);
}Usage
tsx
import { NucleusNavbar } from "@/components/nucleus/nucleus-navbar";
import { NucleusButton } from "@/components/nucleus/nucleus-button";
import { Atom } from "lucide-react";
export function Example() {
return (
<NucleusNavbar
logo={
<span className="flex items-center gap-2 font-semibold">
<Atom className="size-5 text-primary" />
orbit
</span>
}
links={[
{ label: "Docs", href: "/docs" },
{ label: "Components", href: "/components" },
{ label: "Blog", href: "/blog" },
]}
cta={<NucleusButton size="sm">Sign up</NucleusButton>}
/>
);
}Source
Copy this file into components/nucleus/nucleus-navbar.tsx in your project — it's yours to own and modify.
components/nucleus/nucleus-navbar.tsx
"use client";
import * as React from "react";
import { Menu, X } from "lucide-react";
import { cn } from "@/lib/utils";
export interface NucleusNavLink {
label: string;
href: string;
}
export interface NucleusNavbarProps extends React.ComponentProps<"header"> {
/** Logo slot — typically an icon + wordmark. */
logo?: React.ReactNode;
/** Links shown inline on desktop and inside the slide-out on mobile. */
links?: NucleusNavLink[];
/** Call-to-action button rendered at the end (e.g. <NucleusButton>Sign up</NucleusButton>). */
cta?: React.ReactNode;
/** Optional secondary actions rendered next to the CTA on desktop. */
actions?: React.ReactNode;
}
/**
* Responsive navbar: horizontal links on md+, hamburger + slide-out
* panel below md. Uses only standard shadcn tokens; works in light
* and dark mode.
*/
export function NucleusNavbar({
className,
logo,
links = [],
cta,
actions,
...props
}: NucleusNavbarProps) {
const [open, setOpen] = React.useState(false);
// Close the mobile menu on Escape
React.useEffect(() => {
if (!open) return;
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") setOpen(false);
}
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open]);
return (
<header
data-slot="nucleus-navbar"
className={cn(
"sticky top-0 z-40 w-full border-b border-border/60 bg-background/80 backdrop-blur-md",
className
)}
{...props}
>
<div className="mx-auto flex h-14 w-full max-w-6xl items-center justify-between gap-3 px-4 sm:px-6">
<div className="flex min-w-0 items-center gap-2">
{logo}
{/* Desktop links */}
{links.length > 0 && (
<nav aria-label="Main" className="ml-4 hidden items-center gap-1 md:flex">
{links.map((link, i) => (
<a
key={`${link.label}-${link.href}-${i}`}
href={link.href}
className="rounded-lg px-2.5 py-1.5 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
{link.label}
</a>
))}
</nav>
)}
</div>
<div className="flex items-center gap-2">
{actions && <div className="hidden items-center gap-2 md:flex">{actions}</div>}
{cta && <div className="hidden md:block">{cta}</div>}
{/* Mobile hamburger */}
<button
type="button"
aria-label={open ? "Close menu" : "Open menu"}
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
className="inline-flex size-9 items-center justify-center rounded-lg border border-border text-foreground transition-colors hover:bg-muted md:hidden"
>
{open ? <X className="size-4" /> : <Menu className="size-4" />}
</button>
</div>
</div>
{/* Mobile slide-out panel */}
<div
className={cn(
"overflow-hidden border-border/60 transition-[max-height,opacity] duration-200 ease-out md:hidden",
open ? "max-h-96 border-t opacity-100" : "max-h-0 opacity-0"
)}
>
<nav aria-label="Mobile" className="flex flex-col gap-1 px-4 py-3 sm:px-6">
{links.map((link, i) => (
<a
key={`${link.label}-${link.href}-${i}`}
href={link.href}
onClick={() => setOpen(false)}
className="rounded-lg px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
{link.label}
</a>
))}
{(cta || actions) && (
<div className="mt-2 flex flex-col gap-2 border-t border-border/60 pt-3 md:hidden">
{actions}
{cta}
</div>
)}
</nav>
</div>
</header>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
logo | ReactNode | — | Logo slot — typically an icon + wordmark. |
links | { label: string; href: string }[] | [] | Links rendered inline on desktop, in the slide-out on mobile. |
cta | ReactNode | — | Call-to-action button rendered at the end. |
actions | ReactNode | — | Secondary desktop-only actions next to the CTA. |
className | string | — | Extra classes for the header element. |