nucleusUI
All components

NucleusNavbar

NavigationNew

A responsive navbar — horizontal links on desktop, hamburger + slide-out panel on mobile, with logo and CTA slots.

Preview

Installation

  1. 1shadcn/ui initialized (`npx shadcn@latest init`)
  2. 2Tailwind CSS v4 configured with the nucleusUI theme tokens
npm install lucide-react clsx tailwind-merge

Prerequisite — 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

PropTypeDescription
logoReactNodeLogo slot — typically an icon + wordmark.
links{ label: string; href: string }[]Links rendered inline on desktop, in the slide-out on mobile.
ctaReactNodeCall-to-action button rendered at the end.
actionsReactNodeSecondary desktop-only actions next to the CTA.
classNamestringExtra classes for the header element.