Nav

Independent building blocks for navigation menus. Compose them inside any <nav> element.

API Reference

NavMenu

Flex container for links.

PropTypeDefault
spacing"sm" | "md" | "lg""md"
elevatedbooleanfalse

NavMenuLink

Styled navigation link with active state.

PropTypeDefault
hrefstring
variant"solid" | "ghost" | "underline""solid"
isActivebooleanfalse
onClick() => void

Variants

Solid

default
<NavMenu>
  <NavMenuLink href="/" variant="solid">Inactive</NavMenuLink>
  <NavMenuLink href="/docs" variant="solid" isActive>Active</NavMenuLink>
</NavMenu>

Ghost

<NavMenu>
  <NavMenuLink href="/" variant="ghost">Inactive</NavMenuLink>
  <NavMenuLink href="/docs" variant="ghost" isActive>Active</NavMenuLink>
</NavMenu>

Underline

<NavMenu>
  <NavMenuLink href="/" variant="underline">Inactive</NavMenuLink>
  <NavMenuLink href="/docs" variant="underline" isActive>Active</NavMenuLink>
</NavMenu>

Spacing

<NavMenu spacing="sm">...</NavMenu>
<NavMenu spacing="md">...</NavMenu>
<NavMenu spacing="lg">...</NavMenu>

Elevated

Adds a subtle background to the menu container.

<NavMenu elevated>
  <NavMenuLink href="/" variant="solid">Home</NavMenuLink>
  <NavMenuLink href="/docs" variant="solid" isActive>Docs</NavMenuLink>
  <NavMenuLink href="/about" variant="solid">About</NavMenuLink>
</NavMenu>

Basic Usage

import { NavMenu, NavMenuLink } from "@/ui";

<NavMenu>
  <NavMenuLink href="/about">About</NavMenuLink>
  <NavMenuLink href="/contact">Contact</NavMenuLink>
</NavMenu>

Source

src/ui/Nav.tsx
import { type ReactNode } from "react";
import { Link } from "./Link";
import { cn } from "@/utils";

// NavMenu
export interface NavMenuProps {
  children: ReactNode;
  className?: string;
  elevated?: boolean;
  spacing?: "sm" | "md" | "lg";
}

export function NavMenu({
  children,
  className,
  elevated = false,
  spacing = "md",
}: NavMenuProps) {
  const gap =
    spacing === "sm" ? "gap-1" : spacing === "lg" ? "gap-8" : "gap-4";

  return (
    <div
      className={cn(
        "hidden items-center md:flex",
        gap,
        elevated && "rounded-md bg-root-100 p-1",
        className,
      )}
    >
      {children}
    </div>
  );
}

// NavMenuLink
export interface NavMenuLinkProps {
  href: string;
  onClick?: () => void;
  isActive?: boolean;
  variant?: "solid" | "ghost" | "underline";
  className?: string;
  children: ReactNode;
}

export function NavMenuLink({
  href,
  onClick,
  isActive = false,
  variant = "solid",
  className,
  children,
}: NavMenuLinkProps) {
  const isUnderline = variant === "underline";

  const padding = isUnderline ? "py-1" : "px-3.5 py-1.5";

  const activeStyles = (() => {
    switch (variant) {
      case "underline":
        return "border-b border-root-contrast text-root-contrast";
      case "ghost":
        return "bg-root-ghost text-root-contrast";
      default:
        return "bg-root-200 text-root-contrast shadow-sm";
    }
  })();

  const inactiveStyles = cn(
    "text-root-contrast-subtle hover:text-root-contrast",
    isUnderline && "border-b border-transparent",
  );

  return (
    <Link
      href={href}
      onClick={onClick}
      className={cn(
        "text-sm font-medium transition-colors",
        !isUnderline && "rounded-md",
        padding,
        isActive ? activeStyles : inactiveStyles,
        className,
      )}
    >
      {children}
    </Link>
  );
}