Propel

Navigation Menu

A horizontal row of triggers and links that open portaled content panels for site navigation.

Show code
import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuContentList,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuLinkDescription,
  NavigationMenuLinkTitle,
  NavigationMenuList,
  NavigationMenuPanel,
  NavigationMenuTrigger,
  NavigationMenuViewport,
} from "@makeplane/propel/components/navigation-menu";

const PRODUCT_LINKS = [
  { href: "#issues", title: "Issues", description: "Track work items across projects." },
  { href: "#cycles", title: "Cycles", description: "Time-boxed sprints for your team." },
  { href: "#modules", title: "Modules", description: "Group related work into modules." },
  { href: "#pages", title: "Pages", description: "Rich documents that live with your work." },
];

const RESOURCE_LINKS = [
  { href: "#docs", title: "Documentation", description: "Guides and API references." },
  { href: "#changelog", title: "Changelog", description: "What shipped recently." },
];

export default function BasicDemo() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger label="Product" />
          <NavigationMenuContent>
            <NavigationMenuContentList>
              {PRODUCT_LINKS.map((item) => (
                <li key={item.href}>
                  <NavigationMenuLink href={item.href} appearance="card">
                    <NavigationMenuLinkTitle>{item.title}</NavigationMenuLinkTitle>
                    <NavigationMenuLinkDescription>
                      {item.description}
                    </NavigationMenuLinkDescription>
                  </NavigationMenuLink>
                </li>
              ))}
            </NavigationMenuContentList>
          </NavigationMenuContent>
        </NavigationMenuItem>

        <NavigationMenuItem>
          <NavigationMenuTrigger label="Resources" />
          <NavigationMenuContent>
            <NavigationMenuContentList>
              {RESOURCE_LINKS.map((item) => (
                <li key={item.href}>
                  <NavigationMenuLink href={item.href} appearance="card">
                    <NavigationMenuLinkTitle>{item.title}</NavigationMenuLinkTitle>
                    <NavigationMenuLinkDescription>
                      {item.description}
                    </NavigationMenuLinkDescription>
                  </NavigationMenuLink>
                </li>
              ))}
            </NavigationMenuContentList>
          </NavigationMenuContent>
        </NavigationMenuItem>

        <NavigationMenuItem>
          <NavigationMenuLink href="#pricing" appearance="item">
            Pricing
          </NavigationMenuLink>
        </NavigationMenuItem>
      </NavigationMenuList>

      <NavigationMenuPanel>
        <NavigationMenuViewport />
      </NavigationMenuPanel>
    </NavigationMenu>
  );
}

Installation

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuContentList,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuLinkDescription,
  NavigationMenuLinkTitle,
  NavigationMenuList,
  NavigationMenuPanel,
  NavigationMenuTrigger,
  NavigationMenuViewport,
} from "@makeplane/propel/components/navigation-menu";

Usage

import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuContentList,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuLinkDescription,
  NavigationMenuLinkTitle,
  NavigationMenuList,
  NavigationMenuPanel,
  NavigationMenuTrigger,
  NavigationMenuViewport,
} from "@makeplane/propel/components/navigation-menu";

const PRODUCT_LINKS = [
  { href: "#issues", title: "Issues", description: "Track work items across projects." },
  { href: "#cycles", title: "Cycles", description: "Time-boxed sprints for your team." },
  { href: "#modules", title: "Modules", description: "Group related work into modules." },
  { href: "#pages", title: "Pages", description: "Rich documents that live with your work." },
];

const RESOURCE_LINKS = [
  { href: "#docs", title: "Documentation", description: "Guides and API references." },
  { href: "#changelog", title: "Changelog", description: "What shipped recently." },
];

export default function BasicDemo() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger label="Product" />
          <NavigationMenuContent>
            <NavigationMenuContentList>
              {PRODUCT_LINKS.map((item) => (
                <li key={item.href}>
                  <NavigationMenuLink href={item.href} appearance="card">
                    <NavigationMenuLinkTitle>{item.title}</NavigationMenuLinkTitle>
                    <NavigationMenuLinkDescription>
                      {item.description}
                    </NavigationMenuLinkDescription>
                  </NavigationMenuLink>
                </li>
              ))}
            </NavigationMenuContentList>
          </NavigationMenuContent>
        </NavigationMenuItem>

        <NavigationMenuItem>
          <NavigationMenuTrigger label="Resources" />
          <NavigationMenuContent>
            <NavigationMenuContentList>
              {RESOURCE_LINKS.map((item) => (
                <li key={item.href}>
                  <NavigationMenuLink href={item.href} appearance="card">
                    <NavigationMenuLinkTitle>{item.title}</NavigationMenuLinkTitle>
                    <NavigationMenuLinkDescription>
                      {item.description}
                    </NavigationMenuLinkDescription>
                  </NavigationMenuLink>
                </li>
              ))}
            </NavigationMenuContentList>
          </NavigationMenuContent>
        </NavigationMenuItem>

        <NavigationMenuItem>
          <NavigationMenuLink href="#pricing" appearance="item">
            Pricing
          </NavigationMenuLink>
        </NavigationMenuItem>
      </NavigationMenuList>

      <NavigationMenuPanel>
        <NavigationMenuViewport />
      </NavigationMenuPanel>
    </NavigationMenu>
  );
}

Examples

Presentations

A link’s appearance sets its shape: item is a top-level pill beside the triggers, while card stacks a title over an optional description inside a content panel.

Show code
import {
  NavigationMenu,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuLinkDescription,
  NavigationMenuLinkTitle,
  NavigationMenuList,
} from "@makeplane/propel/components/navigation-menu";

export default function PresentationsDemo() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuLink href="#pricing" appearance="item">
            Pricing
          </NavigationMenuLink>
        </NavigationMenuItem>
        <NavigationMenuItem>
          <NavigationMenuLink href="#docs" appearance="card">
            <NavigationMenuLinkTitle>Documentation</NavigationMenuLinkTitle>
            <NavigationMenuLinkDescription>
              Guides and API references.
            </NavigationMenuLinkDescription>
          </NavigationMenuLink>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  );
}

Large content

Cap the content region’s height and wrap the link list in a ScrollArea so the panel keeps a stable size while long menus scroll.

Show code
import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuContentList,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuLinkTitle,
  NavigationMenuList,
  NavigationMenuPanel,
  NavigationMenuTrigger,
  NavigationMenuViewport,
} from "@makeplane/propel/components/navigation-menu";
import { ScrollArea } from "@makeplane/propel/components/scroll-area";

const PROJECT_LINKS = Array.from({ length: 24 }, (_, index) => ({
  href: `#project-${index + 1}`,
  title: `Project ${index + 1}`,
}));

export default function LargeContentDemo() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger label="Projects" />
          <NavigationMenuContent>
            <div className="flex h-72 flex-col">
              <ScrollArea orientation="vertical">
                <NavigationMenuContentList>
                  {PROJECT_LINKS.map((item) => (
                    <li key={item.href}>
                      <NavigationMenuLink href={item.href} appearance="card">
                        <NavigationMenuLinkTitle>{item.title}</NavigationMenuLinkTitle>
                      </NavigationMenuLink>
                    </li>
                  ))}
                </NavigationMenuContentList>
              </ScrollArea>
            </div>
          </NavigationMenuContent>
        </NavigationMenuItem>

        <NavigationMenuItem>
          <NavigationMenuLink href="#pricing" appearance="item">
            Pricing
          </NavigationMenuLink>
        </NavigationMenuItem>
      </NavigationMenuList>

      <NavigationMenuPanel>
        <NavigationMenuViewport />
      </NavigationMenuPanel>
    </NavigationMenu>
  );
}

API Reference

NavigationMenu

The navigation menu Root — Base UI's context/state provider. It renders Base UI's unstyled <nav> container (a <div> when nested) and carries no propel styling, so it is a behavior-only role and lives in components (rules 1a, 2); the styled parts live in elements/navigation-menu and are grafted onto Base UI behavior here.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, NavigationMenuRootState>—Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.

NavigationMenuTrigger

The ready-made trigger row: grafts Base UI's NavigationMenu.Trigger behavior onto the styled trigger, wraps the label, and bakes the rotating disclosure caret (NavigationMenuIcon with a default chevron — defaults are a components concern, rule 2a). Override the glyph via icon.

PropTypeDefaultDescription
label(required)string—Visible trigger label.
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, NavigationMenuTriggerState>—Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.
iconReactNode—Replaces the default disclosure caret (a down chevron) in the trigger's Icon slot.

NavigationMenuLink

The ready-made navigable link: grafts Base UI's NavigationMenu.Link behavior onto the styled link. appearance="item" is a top-level pill beside the triggers; appearance="card" is a stacked entry inside a NavigationMenuContent, pairing a NavigationMenuLinkTitle with an optional NavigationMenuLinkDescription.

PropTypeDefaultDescription
appearance(required)"item" | "card"—
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>, NavigationMenuLinkState>—Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.

NavigationMenuPanel

The navigation menu surface: Base UI portal + positioner + popup grafted onto Propel styling. Named Panel to avoid clashing with Base UI's atomic per-item NavigationMenu.Content. The viewport and per-item content remain children.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>—Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.
side"top" | "bottom" | "left" | "right" | "inline-end" | "inline-start"bottomWhich side of the trigger the panel opens toward.
sideOffsetnumber | OffsetFunction4Distance in px between the trigger and the panel.
align"center" | "start" | "end"startAlignment of the panel relative to the trigger along side.
alignOffsetnumber | OffsetFunction0Additional offset in px along the align axis — slides the panel along the trigger's edge without changing align.
collisionPaddingPadding5Minimum gap in px between the panel and the viewport edge it would otherwise touch.
collisionBoundaryBoundary—The element or rect the panel is confined to when avoiding collisions. Base UI's clipping ancestors when unset.
collisionAvoidanceCollisionAvoidance—What to do when the panel would overflow the boundary, set per axis: flip to the other side, shift along it, or stay put. Base UI flips the side and shifts the alignment when unset.
stickybooleanfalseKeep the panel on screen after the trigger scrolls out of view, instead of following it out.
positionMethod"absolute" | "fixed""absolute"Which CSS position the positioner uses. fixed escapes overflow-clipping ancestors, e.g. a menubar inside a sticky header.
anchorElement | VirtualElement | RefObject<Element | null> | (() => Element | VirtualElement | null) | null—Position against this element instead of the trigger — an element, a ref, a virtual element, or a function returning one. Geometry only; the trigger keeps its role and interactions.
disableAnchorTrackingbooleanfalseStop tracking layout shifts of the anchor — position once, on open.