For AI agents: the complete documentation index is available at https://ifhover.github.io/leaf-ui/en/llms.txt, the full documentation bundle is available at https://ifhover.github.io/leaf-ui/en/llms-full.txt, and this page is available as Markdown at https://ifhover.github.io/leaf-ui/en/components/menu.md.

Menu

Organize hierarchical navigation.

Basic usage

Use itemRender to append status and numeric tags after the menu label. Top-level items keep their icons, while second-level items use text only, aligned with the top-level labels. Submenus use shorter rows and muted text. Hovering and pressing selectable items only changes their text color; the selected item keeps its background, while ancestor menus retain their normal appearance.

basic.tsx
import { Menu, Tag } from '@ifhover/leaf-ui';

import { FolderKanban, House, Settings, Users } from 'lucide-react';

export function MenuBasic() {
  return (
    <Menu
      mode="inline"
      defaultSelectedKey="design"
      defaultOpenKeys={['projects']}
      itemRender={(item, content) => (
        <>
          {content}
          {item.key === 'release' && (
            <Tag size="sm" color="primary">
              New
            </Tag>
          )}
          {item.key === 'team' && (
            <Tag size="sm" color="info">
              12
            </Tag>
          )}
        </>
      )}
      items={[
        { key: 'home', label: 'Overview', icon: <House /> },
        {
          key: 'projects',
          label: 'Projects',
          icon: <FolderKanban />,
          children: [
            { key: 'design', label: 'Design resources' },
            { key: 'release', label: 'Releases' },
          ],
        },
        { key: 'team', label: 'Team members', icon: <Users /> },
        { key: 'settings', label: 'Settings', icon: <Settings /> },
      ]}
    />
  );
}

Use Tabs to switch between vertical, inline, horizontal and collapsed sidebar layouts. Vertical and inline examples start with their submenu expanded. Horizontal navigation and collapsed sidebars show submenus in floating panels. Use the sidebar button to compare its expanded and collapsed states.

advanced.tsx
import type { MenuItem } from '@ifhover/leaf-ui';

import { Button, Flex, Menu, Tabs } from '@ifhover/leaf-ui';

import { Folder, House, Settings } from 'lucide-react';

import { useState } from 'react';

const items: MenuItem[] = [
  { key: 'home', label: 'Overview', icon: <House /> },
  {
    key: 'resources',
    label: 'Resources',
    icon: <Folder />,
    children: [
      { key: 'components', label: 'Components' },
      { key: 'themes', label: 'Themes' },
    ],
  },
  { key: 'settings', label: 'Settings', icon: <Settings /> },
];

export function MenuLayouts() {
  const [collapsed, setCollapsed] = useState(true);
  return (
    <Tabs
      aria-label="Menu layouts"
      items={[
        {
          key: 'vertical',
          label: 'Vertical',
          children: (
            <Menu
              mode="vertical"
              items={items}
              defaultSelectedKey="components"
              defaultOpenKeys={['resources']}
            />
          ),
        },
        {
          key: 'inline',
          label: 'Inline',
          children: (
            <Menu
              mode="inline"
              items={items}
              defaultSelectedKey="components"
              defaultOpenKeys={['resources']}
            />
          ),
        },
        {
          key: 'horizontal',
          label: 'Horizontal',
          children: <Menu mode="horizontal" items={items} defaultSelectedKey="components" />,
        },
        {
          key: 'collapsed',
          label: 'Collapsed',
          children: (
            <Flex direction="column" gap="md" align="start">
              <Button
                variant="soft"
                size="sm"
                aria-expanded={!collapsed}
                onClick={() => setCollapsed((value) => !value)}
              >
                {collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
              </Button>
              <Menu collapsed={collapsed} items={items} defaultSelectedKey="components" />
            </Flex>
          ),
        },
      ]}
    />
  );
}

Vertical menu groups

Use type: 'group' in items, with label for the group title and children for menu items. Titles accept ReactNode and are skipped by selection and arrow navigation. Collapsed menus hide titles while keeping items available.

groups.tsx
import { Menu } from '@ifhover/leaf-ui';

export function MenuGroups() {
  return (
    <Menu
      defaultSelectedKey="overview"
      items={[
        {
          key: 'workspace',
          type: 'group',
          label: 'Workspace',
          children: [
            { key: 'overview', label: 'Overview' },
            { key: 'projects', label: 'Projects' },
          ],
        },
        {
          key: 'account',
          type: 'group',
          label: 'Account',
          children: [{ key: 'settings', label: 'Settings' }],
        },
      ]}
    />
  );
}

Reuse group titles

MenuGroup shares the menu group title style. Its title accepts ReactNode and its children can contain a menu, descriptions, buttons or any custom elements. For groups containing only menu items, prefer the items API above to share selection state and keyboard navigation within one Menu.

Place custom content in a standalone MenuGroup beside Menu so controls keep their own Tab order and keyboard behavior. MenuGroup does not manage selection, expansion or collapse. The application decides whether to hide this section when a sidebar collapses.

Quick actions
Place descriptions, buttons and other custom content below the title.
custom-group.tsx
import { Button, MenuGroup, Space, Text } from '@ifhover/leaf-ui';

import { useState } from 'react';

export function MenuCustomGroup() {
  const [created, setCreated] = useState(false);
  return (
    <MenuGroup title="Quick actions">
      <Space direction="vertical" align="start">
        <Text tone="muted">
          Place descriptions, buttons and other custom content below the title.
        </Text>
        <Button onClick={() => setCreated(true)}>Create project</Button>
        {created && <Text role="status">Project created</Text>}
      </Space>
    </MenuGroup>
  );
}

Controlled navigation and routing

Keep selectedKey in sync with the current route. Use linkRender to replace the default anchor with a React Router or Next.js Link, retaining its event handlers, classes and ARIA attributes.

Current page: overview. The example updates local state; replace the anchor with your router Link and retain its props.
capabilities.tsx
import type { MenuItem } from '@ifhover/leaf-ui';

import { Menu, Space, Text } from '@ifhover/leaf-ui';

import { BookOpen, House } from 'lucide-react';

import { type AnchorHTMLAttributes, cloneElement, isValidElement, type ReactElement, useState } from 'react';

export function MenuRouting() {
  const [selected, setSelected] = useState('overview');
  const items: MenuItem[] = [
    { key: 'overview', label: 'Overview', href: '#overview', icon: <House /> },
    {
      key: 'resources',
      label: 'Resources',
      icon: <BookOpen />,
      children: [
        { key: 'guides', label: 'Guides', href: '#guides' },
        {
          key: 'settings',
          label: 'Settings',
          href: '#settings',
        },
      ],
    },
  ];
  return (
    <Space direction="vertical" align="center">
      <Menu
        aria-label={'Application navigation'}
        items={items}
        selectedKey={selected}
        defaultOpenKeys={['resources']}
        onSelect={setSelected}
        linkRender={(_item, link) => {
          if (!isValidElement(link)) return link;
          const element = link as ReactElement<AnchorHTMLAttributes<HTMLAnchorElement>>;
          return cloneElement(element, {
            onClick: (event) => {
              element.props.onClick?.(event);
              event.preventDefault();
            },
          });
        }}
      />
      <Text tone="muted">
        {`Current page: ${selected}. The example updates local state; replace the anchor with your router Link and retain its props.`}
      </Text>
    </Space>
  );
}

Usage

The vertical menu defaults to 240px (60px when collapsed); use style.width to customize it. items supports nesting, groups, dividers and links. selectedKey controls the current item and openKeys controls expanded items. Horizontal and collapsed modes display submenus in floating panels.

Horizontal submenus open below their trigger. Collapsed sidebar submenus open to the side with a parent title. Popups share aligned icons, labels, group headings and selected backgrounds; only the current page is highlighted, while its ancestors retain their normal appearance. RTL reverses the side placement, and popups adjust near viewport edges.

Submenus animate in both directions, and the sidebar width transitions when collapsed. Closed items are excluded from keyboard navigation. Transitions are disabled when the system requests reduced motion.

Menu items, hover backgrounds and selection highlights share the base radius with buttons. Set ConfigProvider's theme.borderRadius to adjust them together, or use theme.components.Menu.borderRadius to customize the menu radius.

Hovering, focusing and pressing selectable items changes only their text and icon colors, preserving the background and the selected item's primary fill. Danger items use danger colors for feedback. Submenu triggers retain a subtle hover background that deepens on press. Submenu popups follow the same rules. Submenu rows default to 32px (26px in compact density) and follow the regional control height.

Types

Import these types for data, callbacks and refs.

import type { MenuGroupProps, MenuItem, MenuProps } from '@ifhover/leaf-ui';
PropertyTypeDefaultDescription
keystringRequiredRequired. Unique stable identifier
labelReactNodeRequiredRequired. Display label
iconReactNode—Custom icon
hrefstring—Destination URL
disabledboolean—Disable interaction.
dangerboolean—Danger colors
childrenreadonly MenuItem[]—Content or child items
type'item' | 'group' | 'divider'—Visual or item type
PropertyTypeDefaultDescription
itemsreadonly MenuItem[]RequiredRequired. Items; keep keys unique and stable
mode'vertical' | 'horizontal' | 'inline''vertical'Menu orientation and behavior
selectedKeystring—Controlled selected key
defaultSelectedKeystring''Initial selected key
onSelect(key: string, item: MenuItem) => void—Select an item
openKeysreadonly string[]—Controlled open keys
defaultOpenKeysreadonly string[][]Initially open keys
onOpenChange(keys: string[]) => void—Open state changes
collapsedbooleanfalseControlled collapsed state
itemRender(item: MenuItem, content: ReactNode) => ReactNode—Customize item content.
linkRender(item: MenuItem, link: ReactNode) => ReactNode—Render a router link while retaining supplied accessibility and event props.
itemClassNamestring | ((item: MenuItem) => string)—Item class, or a class computed from an item.

Accepts native div attributes and a ref to HTMLDivElement. The title provides an accessible name; override it with aria-label or aria-labelledby. Import from the package root or @ifhover/leaf-ui/menu.

PropertyTypeDefaultDescription
titleReactNodeRequiredRequired. Group title, sharing the style used by Menu groups
childrenReactNode—Menu or any custom content