Menu

Combine a list of secondary actions into single interactive area

PackageIcon

Usage

import { Menu, Button, Text } from '@mantine/core';
import { GearSixIcon, MagnifyingGlassIcon, ImageIcon, ChatCircleIcon, TrashIcon, IconArrowsLeftRight } from '@phosphor-icons/react';

function Demo() {
  return (
    <Menu shadow="md" width={200}>
      <Menu.Target>
        <Button>Toggle menu</Button>
      </Menu.Target>

      <Menu.Dropdown>
        <Menu.Label>Application</Menu.Label>
        <Menu.Item leftSection={<GearSixIcon size={14} />}>
          Settings
        </Menu.Item>
        <Menu.Item leftSection={<ChatCircleIcon size={14} />}>
          Messages
        </Menu.Item>
        <Menu.Item leftSection={<ImageIcon size={14} />}>
          Gallery
        </Menu.Item>
        <Menu.Item
          leftSection={<MagnifyingGlassIcon size={14} />}
          rightSection={
            <Text size="xs" c="dimmed">
              ⌘K
            </Text>
          }
        >
          Search
        </Menu.Item>

        <Menu.Divider />

        <Menu.Label>Danger zone</Menu.Label>
        <Menu.Item
          leftSection={<IconArrowsLeftRight size={14} />}
        >
          Transfer my data
        </Menu.Item>
        <Menu.Item
          color="red"
          leftSection={<TrashIcon size={14} />}
        >
          Delete my account
        </Menu.Item>
      </Menu.Dropdown>
    </Menu>
  );
}

Submenus

import { Button, Menu } from '@mantine/core';

function Demo() {
  return (
    <Menu width={200} position="bottom-start">
      <Menu.Target>
        <Button>Toggle Menu</Button>
      </Menu.Target>

      <Menu.Dropdown>
        <Menu.Item>Dashboard</Menu.Item>

        <Menu.Sub openDelay={120} closeDelay={150}>
          <Menu.Sub.Target>
            <Menu.Sub.Item>Products</Menu.Sub.Item>
          </Menu.Sub.Target>

          <Menu.Sub.Dropdown>
            <Menu.Item>All products</Menu.Item>
            <Menu.Item>Categories</Menu.Item>
            <Menu.Item>Tags</Menu.Item>
            <Menu.Item>Attributes</Menu.Item>
            <Menu.Item>Shipping classes</Menu.Item>
          </Menu.Sub.Dropdown>
        </Menu.Sub>

        <Menu.Sub>
          <Menu.Sub.Target>
            <Menu.Sub.Item>Orders</Menu.Sub.Item>
          </Menu.Sub.Target>

          <Menu.Sub.Dropdown>
            <Menu.Item>Open</Menu.Item>
            <Menu.Item>Completed</Menu.Item>
            <Menu.Item>Cancelled</Menu.Item>
          </Menu.Sub.Dropdown>
        </Menu.Sub>

        <Menu.Sub>
          <Menu.Sub.Target>
            <Menu.Sub.Item>Settings</Menu.Sub.Item>
          </Menu.Sub.Target>

          <Menu.Sub.Dropdown>
            <Menu.Item>Profile</Menu.Item>
            <Menu.Item>Security</Menu.Item>
            <Menu.Item>Notifications</Menu.Item>
          </Menu.Sub.Dropdown>
        </Menu.Sub>
      </Menu.Dropdown>
    </Menu>
  );
}

Controlled

The dropdown's opened state can be controlled with the opened and onChange props:

import { useState } from 'react';
import { Menu } from '@mantine/core';

function Demo() {
  const [opened, setOpened] = useState(false);
  return (
    <Menu opened={opened} onChange={setOpened}>
      {/* Menu content */}
    </Menu>
  );
}

Show menu on hover

Set trigger="hover" to reveal the dropdown when hovering over the menu target and dropdown. The closeDelay and openDelay props can be used to control open and close delay in ms. Note that:

  • If you set closeDelay={0} then the menu will close before the user reaches the dropdown, so set offset={0} to remove space between the target element and dropdown.
  • Menu with trigger="hover" is not accessible – users that navigate with the keyboard will not be able to use it. If you need both hover and click triggers, use trigger="click-hover".
import { Menu } from '@mantine/core';

function Demo() {
  return (
    <Menu trigger="hover" openDelay={100} closeDelay={400}>
      {/* ... menu items */}
    </Menu>
  );
}

To make a Menu that is revealed on hover accessible on all devices, use trigger="click-hover" instead. The dropdown will be revealed on hover on desktop and on click on mobile devices.

import { Menu } from '@mantine/core';

function Demo() {
  return (
    <Menu trigger="click-hover" openDelay={100} closeDelay={400}>
      {/* ... menu items */}
    </Menu>
  );
}

Disabled items

import { Menu, Button } from '@mantine/core';
import { MagnifyingGlassIcon } from '@phosphor-icons/react';

function Demo() {
  return (
    <Menu>
      <Menu.Target>
        <Button>Toggle menu</Button>
      </Menu.Target>

      <Menu.Dropdown>
        <Menu.Item
          leftSection={<MagnifyingGlassIcon size={14} />}
          disabled
        >
          Search
        </Menu.Item>

        {/* Other items ... */}
      </Menu.Dropdown>
    </Menu>
  );
}

Dropdown position

Offset
import { Menu } from '@mantine/core';

function Demo() {
  return (
    <Menu>
      {/* Menu items */}
    </Menu>
  );
}

Transitions

The Menu dropdown can be animated with any of the premade transitions from the Transition component:

import { Menu } from '@mantine/core';

function Demo() {
  return (
    <Menu transitionProps={{ transition: 'rotate-right', duration: 150 }}>
      {/* Menu content */}
    </Menu>
  );
}

Custom component as Menu.Item

By default, Menu.Item renders as a button element. To change that, set the component prop:

import { Menu, Button } from '@mantine/core';
import { ArrowSquareOutIcon } from '@phosphor-icons/react';

function Demo() {
  return (
    <Menu width={200} shadow="md">
      <Menu.Target>
        <Button>Toggle menu</Button>
      </Menu.Target>

      <Menu.Dropdown>
        <Menu.Item component="a" href="https://mantine.dev">
          Mantine website
        </Menu.Item>
        <Menu.Item
          leftSection={<ArrowSquareOutIcon size={14} />}
          component="a"
          href="https://mantine.dev"
          target="_blank"
        >
          External link
        </Menu.Item>
      </Menu.Dropdown>
    </Menu>
  );
}

Note that the component you pass to the component prop should allow spreading props to its root element:

import { Menu } from '@mantine/core';

// ❌ Will not work with Menu.Item
function IncorrectItem() {
  return <button type="button">My custom Menu item</button>;
}

// ✅ Will work correctly with Menu.Item
const CorrectItem = ({ ref, ...props }) => (
  <button type="button" {...props} ref={ref}>
    My custom Menu item
  </button>
);

function Demo() {
  // ❌ Will not work
  const incorrect = <Menu.Item component={IncorrectItem} />;

  // ✅ Will work
  const correct = <Menu.Item component={CorrectItem} />;
}

Custom component as target

import { CaretRightIcon } from '@phosphor-icons/react';
import { Group, Avatar, Text, Menu, UnstyledButton } from '@mantine/core';

interface UserButtonProps extends React.ComponentProps<'button'> {
  image: string;
  name: string;
  email: string;
  icon?: React.ReactNode;
}

function UserButton({ image, name, email, icon, ...others }: UserButtonProps) {
  return (
    <UnstyledButton
      style={{
        padding: 'var(--mantine-spacing-md)',
        color: 'var(--mantine-color-text)',
        borderRadius: 'var(--mantine-radius-sm)',
      }}
      {...others}
    >
      <Group>
        <Avatar src={image} radius="xl" />

        <div style={{ flex: 1 }}>
          <Text size="sm" fw={500}>
            {name}
          </Text>

          <Text c="dimmed" size="xs">
            {email}
          </Text>
        </div>

        {icon || <CaretRightIcon size={16} />}
      </Group>
    </UnstyledButton>
  );
}

function Demo() {
  return (
    <Menu withArrow>
      <Menu.Target>
        <UserButton
          image="https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/avatars/avatar-8.png"
          name="Harriette Spoonlicker"
          email="hspoonlicker@outlook.com"
        />
      </Menu.Target>
      {/* ... menu items */}
    </Menu>
  );
}

Styles API

Menu supports the Styles API; you can add styles to any inner element of the component with the classNames prop. Follow the Styles API documentation to learn more.

Component Styles API

Hover over selectors to highlight corresponding elements

/*
 * Hover over selectors to apply outline styles
 *
 */

Menu.Target children

Menu.Target requires an element or a component as a single child – strings, fragments, numbers, and multiple elements/components are not supported and will throw an error. Custom components must provide a prop to get the root element ref; all Mantine components support ref out of the box.

import { Menu, Button } from '@mantine/core';

function Demo() {
  return (
    <>
      <Menu.Target>
        <button>Native button – ok</button>
      </Menu.Target>

      {/* OK */}
      <Menu.Target>
        <Button>Mantine component – ok</Button>
      </Menu.Target>

      {/* String, NOT OK – will throw error */}
      <Menu.Target>Raw string</Menu.Target>

      {/* Number, NOT OK – will throw error */}
      <Menu.Target>{2}</Menu.Target>

      {/* Fragment, NOT OK – will throw error */}
      <Menu.Target>
        <>Fragment, NOT OK, will throw error</>
      </Menu.Target>

      {/* Multiple nodes, NOT OK – will throw error */}
      <Menu.Target>
        <div>More that one node</div>
        <div>NOT OK, will throw error</div>
      </Menu.Target>
    </>
  );
}

Required ref prop

Custom components that are rendered inside Menu.Target are required to support the ref prop:

// Example of code that WILL NOT WORK
import { Menu } from '@mantine/core';

// ❌ ref is not forwarded to the root element
function MyComponent() {
  return <div>My component</div>;
}

// This will not work – MyComponent does not support ref
function Demo() {
  return (
    <Menu>
      <Menu.Target>
        <MyComponent />
      </Menu.Target>
    </Menu>
  );
}

Pass ref to the root element:

// Example of code that will work
import { Menu } from '@mantine/core';

// ✅ ref is forwarded to the root element
function MyComponent({ ref, ...others }: React.ComponentProps<'div'>) {
  return <div ref={ref} {...others}>My component</div>;
}

// Works correctly – ref is forwarded
function Demo() {
  return (
    <Menu>
      <Menu.Target>
        <MyComponent />
      </Menu.Target>
    </Menu>
  );
}

Accessibility

Menu follows WAI-ARIA recommendations:

  • Dropdown element has role="menu" and aria-labelledby="target-id" attributes
  • Target element has aria-haspopup="menu", aria-expanded, aria-controls="dropdown-id" attributes
  • Menu item has role="menuitem" attribute

Whilst the dropdown is unopened, the aria-controls attribute will be undefined

Supported target elements

An uncontrolled Menu with trigger="click" (default) will be accessible only when used with a button element or component that renders it (Button, ActionIcon, etc.). Other elements will not support Space and Enter key presses.

Hover menu

Menu with trigger="hover" is not accessible – it cannot be accessed with the keyboard. Use it only if you do not care about accessibility. If you need both hover and click triggers, use trigger="click-hover".

Navigation

If you are using the Menu to build navigation, you can use the options from the demo below to follow the WAI-ARIA recommendations for navigation.

import { Group, Menu } from '@mantine/core';

function Demo() {
  const menus = Array(4)
    .fill(0)
    .map((e, i) => (
      <Menu
        key={i}
        trigger="click-hover"
        loop={false}
        withinPortal={false}
        trapFocus={false}
        menuItemTabIndex={0}
      >
        {/* ... menu items */}
      </Menu>
    ));
  return <Group>{menus}</Group>;
}

Keyboard interactions

KeyDescriptionCondition
EscapeCloses dropdownFocus within dropdown
Space/EnterOpens/closes dropdownFocus on target element
ArrowUpMoves focus to previous menu itemFocus within dropdown
ArrowDownMoves focus to next menu itemFocus within dropdown
HomeMoves focus to first menu itemFocus within dropdown
EndMoves focus to last menu itemFocus within dropdown

If you also need to support Tab and Shift + Tab then set menuItemTabIndex={0}.