Menu

free

Menu gives sidebar and nested navigation a clear active route context.

motion
Preview
Dark
import { type ReactNode } from 'react';
import { PiBell, PiCreditCard, PiGear, PiUser, PiUsers } from 'react-icons/pi'
import Menu from '@/components/ui/Menu';

const MenuLabel = ({
  icon,
  children,
}: {
  icon: ReactNode;
  children: ReactNode;
}) => (
  <span className="flex min-w-0 items-center gap-2">
    {icon}
    <span className="truncate">{children}</span>
  </span>
);

export default function UsageDemo() {
  return (
    <div className="w-full max-w-[250px] rounded-card bg-card p-2">
      <Menu
        variant="subtle"
        defaultActiveKeys={['profile']}
        defaultExpandedKeys={['account']}
        defaultCollapseActiveKeys={['account']}
      >
        <Menu.MenuItem eventKey="profile">
          <MenuLabel icon={<PiUser className="text-base" />}>
            Profile
          </MenuLabel>
        </Menu.MenuItem>
        <Menu.MenuItem eventKey="notifications">
          <MenuLabel icon={<PiBell className="text-base" />}>
            Notifications
          </MenuLabel>
        </Menu.MenuItem>
        <Menu.MenuCollapse
          eventKey="account"
          label={
            <MenuLabel icon={<PiGear className="text-base" />}>
              Account
            </MenuLabel>
          }
        >
          <Menu.MenuItem eventKey="billing">
            <MenuLabel icon={<PiCreditCard className="text-base" />}>
              Billing
            </MenuLabel>
          </Menu.MenuItem>
          <Menu.MenuItem eventKey="team">
            <MenuLabel icon={<PiUsers className="text-base" />}>
              Team
            </MenuLabel>
          </Menu.MenuItem>
        </Menu.MenuCollapse>
      </Menu>
    </div>
  );
}

Installation

Add this component with the NateUI CLI.

npx nateui@latest add Menu

Examples

Basic

Preview
Dark
import Menu from '@/components/ui/Menu';

export default function BasicDemo() {
  return (
    <div className="w-full max-w-[250px] rounded-card bg-card p-2">
      <Menu>
        <Menu.MenuItem eventKey="overview">Overview</Menu.MenuItem>
        <Menu.MenuItem eventKey="customers">Customers</Menu.MenuItem>
        <Menu.MenuItem eventKey="orders">Orders</Menu.MenuItem>
      </Menu>
    </div>
  );
}

Variant

Preview
Dark
import Menu from '@/components/ui/Menu';

const items = ['Profile', 'Billing', 'Team'];

export default function VariantDemo() {
  return (
    <div className="w-full max-w-[250px] space-y-4">
      <div className="rounded-card bg-card p-2">
        <Menu defaultActiveKeys={['Profile']}>
          {items.map((item) => (
            <Menu.MenuItem key={item} eventKey={item}>
              {item}
            </Menu.MenuItem>
          ))}
        </Menu>
      </div>

      <div className="rounded-card bg-card p-2">
        <Menu variant="subtle" defaultActiveKeys={['Profile']}>
          {items.map((item) => (
            <Menu.MenuItem key={item} eventKey={item}>
              {item}
            </Menu.MenuItem>
          ))}
        </Menu>
      </div>
    </div>
  );
}

With Icons

Preview
Dark
import { type ReactNode } from 'react';
import { PiArchive, PiEnvelope, PiGear } from 'react-icons/pi'
import Menu from '@/components/ui/Menu';

const MenuLabel = ({
  icon,
  children,
}: {
  icon: ReactNode;
  children: ReactNode;
}) => (
  <span className="flex items-center gap-2">
    {icon}
    {children}
  </span>
);

export default function WithIconsDemo() {
  return (
    <div className="w-full max-w-[250px] rounded-card bg-card p-2">
      <Menu variant="subtle" defaultActiveKeys={['inbox']}>
        <Menu.MenuItem eventKey="settings">
          <MenuLabel icon={<PiGear className="text-base" />}>
            Settings
          </MenuLabel>
        </Menu.MenuItem>
        <Menu.MenuItem eventKey="inbox">
          <MenuLabel icon={<PiEnvelope className="text-base" />}>
            Inbox
          </MenuLabel>
        </Menu.MenuItem>
        <Menu.MenuItem eventKey="archive">
          <MenuLabel icon={<PiArchive className="text-base" />}>
            Archive
          </MenuLabel>
        </Menu.MenuItem>
      </Menu>
    </div>
  );
}

Collapsible

Preview
Dark
import { type ReactNode } from 'react';
import {
    PiArchive,
    PiEnvelope,
    PiFolderOpen,
    PiGear,
    PiTray,
} from 'react-icons/pi'
import Menu from '@/components/ui/Menu';

const MenuLabel = ({
  icon,
  children,
}: {
  icon: ReactNode;
  children: ReactNode;
}) => (
  <span className="flex items-center gap-2">
    {icon}
    {children}
  </span>
);

export default function CollapsibleDemo() {
  return (
    <div className="w-full max-w-[250px] rounded-card bg-card p-2">
      <Menu defaultExpandedKeys={['mail']}>
        <Menu.MenuItem eventKey="settings">
          <MenuLabel icon={<PiGear className="text-base" />}>
            Settings
          </MenuLabel>
        </Menu.MenuItem>
        <Menu.MenuCollapse
          eventKey="mail"
          label={
            <MenuLabel icon={<PiEnvelope className="text-base" />}>
              Mail
            </MenuLabel>
          }
        >
          <Menu.MenuItem eventKey="inbox">
            <MenuLabel icon={<PiTray className="text-base" />}>
              Inbox
            </MenuLabel>
          </Menu.MenuItem>
          <Menu.MenuItem eventKey="archive">
            <MenuLabel icon={<PiArchive className="text-base" />}>
              Archive
            </MenuLabel>
          </Menu.MenuItem>
          <Menu.MenuCollapse
            eventKey="folders"
            label={
              <MenuLabel icon={<PiFolderOpen className="text-base" />}>
                Folders
              </MenuLabel>
            }
          >
            <Menu.MenuItem eventKey="priority">Priority</Menu.MenuItem>
            <Menu.MenuItem eventKey="follow-up">Follow-up</Menu.MenuItem>
          </Menu.MenuCollapse>
        </Menu.MenuCollapse>
      </Menu>
    </div>
  );
}

Groups

Preview
Dark
import Menu from '@/components/ui/Menu';

export default function GroupsDemo() {
  return (
    <div className="w-full max-w-[250px] rounded-card bg-card p-2">
      <Menu variant="subtle" defaultActiveKeys={['members']}>
        <Menu.MenuGroup label="Workspace">
          <Menu.MenuItem eventKey="overview">Overview</Menu.MenuItem>
          <Menu.MenuItem eventKey="settings">Settings</Menu.MenuItem>
        </Menu.MenuGroup>

        <Menu.MenuGroup label="Team">
          <Menu.MenuItem eventKey="members">Members</Menu.MenuItem>
          <Menu.MenuItem eventKey="roles">Roles</Menu.MenuItem>
        </Menu.MenuGroup>
      </Menu>
    </div>
  );
}

Active And Expanded

Preview
Dark
import Menu from '@/components/ui/Menu';

export default function ActiveExpandedDemo() {
  return (
    <div className="w-full max-w-[250px] rounded-card bg-card p-2">
      <Menu
        defaultActiveKeys={['contacts', 'review']}
        defaultExpandedKeys={['customers']}
        defaultCollapseActiveKeys={['customers']}
      >
        <Menu.MenuItem eventKey="dashboard">Dashboard</Menu.MenuItem>
        <Menu.MenuCollapse eventKey="customers" label="Customers">
          <Menu.MenuItem eventKey="contacts">Contacts</Menu.MenuItem>
          <Menu.MenuItem eventKey="segments">Segments</Menu.MenuItem>
        </Menu.MenuCollapse>
        <Menu.MenuItem eventKey="review" isActive>
          Review Queue
        </Menu.MenuItem>
      </Menu>
    </div>
  );
}

Disabled

Preview
Dark
import Menu from '@/components/ui/Menu';

export default function DisabledDemo() {
  return (
    <div className="w-full max-w-[250px] rounded-card bg-card p-2">
      <Menu>
        <Menu.MenuItem eventKey="profile">Profile</Menu.MenuItem>
        <Menu.MenuItem disabled eventKey="billing">
          Billing
        </Menu.MenuItem>
        <Menu.MenuItem eventKey="team">Team</Menu.MenuItem>
      </Menu>
    </div>
  );
}

API

Menu is a compound component: Menu provides shared active, expanded, height, and variant context; Menu.MenuItem, Menu.MenuCollapse, and Menu.MenuGroup render the list structure.

PropDescriptionTypeDefault
childrenMenu item, group, or collapse content rendered inside the root list.ReactNode-
classNameClass names applied to the root ul.string-
collapsedSwitches menu context into collapsed mode for group labels and collapse indicators.booleanfalse
defaultActiveKeysKeys matched against descendant menu items to mark them active.string[][]
defaultCollapseActiveKeysKeys matched against collapse rows to mark those rows active.string[][]
defaultExpandedKeysCollapse keys opened when the menu renders or when the key list changes.string[][]
menuItemHeightShared item and collapse trigger height, in pixels.number36
onSelectCalled when a menu item selects an eventKey.MenuSelectHandler-
refRef forwarded to the root ul.Ref<HTMLUListElement>-
variantVisual treatment for active and hover states.MenuVariant'default'
...nativePropsNative props applied to the root ul.NativeMenuProps-
PropDescriptionTypeDefault
asElementElement or component rendered for the item.ElementType'li'
childrenItem label or custom content.ReactNode-
classNameClass names merged with the item state classes.string-
disabledApplies disabled styling and data-disabled to the item.boolean-
eventKeySelection key passed to onSelect and matched by active key props.string-
hierarchyIndicatorShows the hierarchy indicator line before the item content.boolean-
indicatorClassNameClass names applied to the hierarchy indicator wrapper.string-
indicatorLineClassNameClass names applied to the hierarchy indicator line.string-
isActiveForces the item into the active state independent of defaultActiveKeys.boolean-
menuItemHeightItem height. Falls back to Menu context height.string | numberMenu menuItemHeight
onSelectItem-level select handler. Falls back to Menu context onSelect.MenuSelectHandlerMenu onSelect
refRef forwarded to the rendered item element.Ref<HTMLElement>-
...nativePropsNative props applied to the rendered item element.NativeMenuItemProps-
PropDescriptionTypeDefault
activeMarks the collapse trigger row active.boolean-
childrenNested menu items, groups, or collapse rows.ReactNode-
classNameClass names applied to the collapse trigger row.string-
eventKeyKey matched by defaultExpandedKeys and defaultCollapseActiveKeys.string-
expandedOpens the collapse row by default.booleanfalse
hierarchyIndicatorShows the hierarchy indicator before the trigger label.boolean-
indentIndents the nested content list.booleantrue
labelTrigger row label.string | ReactNodenull
onToggleCalled when the trigger row changes expanded state.MenuToggleHandler-
...nativePropsNative props applied to the root li.NativeMenuCollapseProps-
PropDescriptionTypeDefault
childrenMenu items or collapse rows rendered inside the group list.ReactNode-
classNameClass names applied to the group wrapper.string-
labelGroup heading content. Required.string | ReactNode-
...nativePropsNative props applied to the group wrapper.NativeMenuGroupProps-

Types

type MenuVariant = 'default' | 'subtle';
 
type MenuSelectHandler = (eventKey: string, e: MouseEvent) => void;
 
type MenuToggleHandler = (
  expanded: boolean,
  e: MouseEvent<HTMLDivElement>,
) => void;
 
type NativeMenuProps = ComponentProps<'ul'>;
type NativeMenuItemProps = ComponentProps<ElementType>;
type NativeMenuCollapseProps = ComponentProps<'li'>;
type NativeMenuGroupProps = ComponentProps<'div'>;