> Leaf UI 0.8.1 · Package: @ifhover/leaf-ui · Source: 435bab23cad7d343653d9b63b049c0b0601eeefb
> Browser documentation: https://ifhover.github.io/leaf-ui/en/components/menu.html

> 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.

# 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.


Overview

Projects

Design resources

ReleasesNew

Team members12

Settings

basic.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/en/menu/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 /> },
      ]}
    />
  );
}
```

Expand code

## Menu layouts

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.


Vertical

Inline

Horizontal

Collapsed

Overview

Resources

Components

Themes

Settings

advanced.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/en/menu/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>
          ),
        },
      ]}
    />
  );
}
```

Expand code

## 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.


Workspace

Overview

Projects

Account

Settings

groups.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/en/menu/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' }],
        },
      ]}
    />
  );
}
```

Expand code

## 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.Create project

custom-group.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/en/menu/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>
  );
}
```

Expand code

## 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.


[Overview](#overview)

Resources

[Guides](#guides)

[Settings](#settings)

Current page: overview. The example updates local state; replace the anchor with your router Link and retain its props.

capabilities.tsx

Copy code

```tsx lineNumbers file="../../../src/examples/menu/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>
  );
}
```

Expand code

## 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.

```ts
import type { MenuGroupProps, MenuItem, MenuProps } from '@ifhover/leaf-ui';
```

## MenuItem API

| Property   | Type                                       | Default  | Description                        |
| ---------- | ------------------------------------------ | -------- | ---------------------------------- |
| `key`      | `string`                                   | Required | Required. Unique stable identifier |
| `label`    | `ReactNode`                                | Required | Required. Display label            |
| `icon`     | `ReactNode`                                | —        | Custom icon                        |
| `href`     | `string`                                   | —        | Destination URL                    |
| `disabled` | `boolean`                                  | —        | Disable interaction.               |
| `danger`   | `boolean`                                  | —        | Danger colors                      |
| `children` | `readonly `[`MenuItem`](#menuitem-api)`[]` | —        | Content or child items             |
| `type`     | `'item' \| 'group' \| 'divider'`           | —        | Visual or item type                |

## MenuProps API

| Property             | Type                                                                     | Default      | Description                                                                  |
| -------------------- | ------------------------------------------------------------------------ | ------------ | ---------------------------------------------------------------------------- |
| `items`              | `readonly `[`MenuItem`](#menuitem-api)`[]`                               | Required     | Required. Items; keep keys unique and stable                                 |
| `mode`               | `'vertical' \| 'horizontal' \| 'inline'`                                 | `'vertical'` | Menu orientation and behavior                                                |
| `selectedKey`        | `string`                                                                 | —            | Controlled selected key                                                      |
| `defaultSelectedKey` | `string`                                                                 | `''`         | Initial selected key                                                         |
| `onSelect`           | `(key: string, item: `[`MenuItem`](#menuitem-api)`) => void`             | —            | Select an item                                                               |
| `openKeys`           | `readonly string[]`                                                      | —            | Controlled open keys                                                         |
| `defaultOpenKeys`    | `readonly string[]`                                                      | `[]`         | Initially open keys                                                          |
| `onOpenChange`       | `(keys: string[]) => void`                                               | —            | Open state changes                                                           |
| `collapsed`          | `boolean`                                                                | `false`      | Controlled collapsed state                                                   |
| `itemRender`         | `(item: `[`MenuItem`](#menuitem-api)`, content: ReactNode) => ReactNode` | —            | Customize item content.                                                      |
| `linkRender`         | `(item: `[`MenuItem`](#menuitem-api)`, link: ReactNode) => ReactNode`    | —            | Render a router link while retaining supplied accessibility and event props. |
| `itemClassName`      | `string \| ((item: `[`MenuItem`](#menuitem-api)`) => string)`            | —            | Item class, or a class computed from an item.                                |

## MenuGroup API

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`.

| Property   | Type        | Default  | Description                                                  |
| ---------- | ----------- | -------- | ------------------------------------------------------------ |
| `title`    | `ReactNode` | Required | Required. Group title, sharing the style used by Menu groups |
| `children` | `ReactNode` | —        | Menu or any custom content                                   |
