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)stringVisible 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.
iconReactNodeReplaces 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<HTMLProps, 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.
collisionBoundaryBoundaryThe element or rect the panel is confined to when avoiding collisions. Base UI's clipping ancestors when unset.
collisionAvoidanceCollisionAvoidanceWhat 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) | nullPosition 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.