Menu

Combine a list of secondary actions into single interactive area

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.Item>Customers</Menu.Item>
        <Menu.Item>Reports</Menu.Item>

        <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>
  );
}

Use safeAreaPolygon to keep a submenu open while the cursor moves from the target toward the dropdown. Pass an object to adjust Floating UI safePolygon options, for example when offset creates a larger gap between the target and dropdown.

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 offset={16} safeAreaPolygon={{ buffer: 16, requireIntent: false }}>
          <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.Item>Customers</Menu.Item>
        <Menu.Item>Reports</Menu.Item>
      </Menu.Dropdown>
    </Menu>
  );
}

Search

Menu.Search renders a search input inside the dropdown. Focus stays on the input while ArrowUp/ArrowDown move a highlight through items, and Enter triggers the highlighted item. Filtering is controlled by the user – pass value and onChange and filter Menu.Item children based on the query. By default, the search value is cleared automatically after the menu close transition completes – disable with clearSearchOnClose={false} if you want to preserve the query between openings.

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

const data = [
  'Dashboard',
  'Customers',
  'Products',
  'Orders',
  'Reports',
  'Settings',
  'Integrations',
  'Billing',
  'Team members',
  'Help center',
];

function Demo() {
  const [query, setQuery] = useState('');
  const items = data.filter((item) => item.toLowerCase().includes(query.toLowerCase().trim()));

  return (
    <Menu shadow="md" width={240}>
      <Menu.Target>
        <Button>Toggle menu</Button>
      </Menu.Target>

      <Menu.Dropdown>
        <Menu.Search
          value={query}
          onChange={(event) => setQuery(event.currentTarget.value)}
          placeholder="Search items"
        />

        {items.length > 0 ? (
          items.map((item) => <Menu.Item key={item}>{item}</Menu.Item>)
        ) : (
          <Text c="dimmed" size="sm" ta="center" py="xs">
            Nothing found
          </Text>
        )}
      </Menu.Dropdown>
    </Menu>
  );
}

The same approach works with submenus. To keep a parent visible when a nested item matches, include the parent in the filtered tree if any of its descendants match the query:

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

interface MenuNode {
  label: string;
  children?: MenuNode[];
}

const data: MenuNode[] = [
  { label: 'Dashboard' },
  { label: 'Customers' },
  {
    label: 'Products',
    children: [
      { label: 'All products' },
      { label: 'Categories' },
      { label: 'Tags' },
      { label: 'Inventory' },
    ],
  },
  { label: 'Orders' },
  {
    label: 'Settings',
    children: [
      {
        label: 'Account',
        children: [
          { label: 'Profile' },
          { label: 'Security' },
          { label: 'Two-factor authentication' },
        ],
      },
      { label: 'Notifications' },
      { label: 'Billing' },
    ],
  },
];

function filterTree(nodes: MenuNode[], query: string): MenuNode[] {
  const q = query.toLowerCase().trim();
  if (!q) {
    return nodes;
  }
  return nodes.reduce<MenuNode[]>((acc, node) => {
    const labelMatches = node.label.toLowerCase().includes(q);
    const children = node.children ? filterTree(node.children, query) : undefined;
    if (labelMatches || (children && children.length > 0)) {
      acc.push({ ...node, children: node.children ? children : undefined });
    }
    return acc;
  }, []);
}

function renderNode(node: MenuNode) {
  if (node.children) {
    return (
      <Menu.Sub key={node.label}>
        <Menu.Sub.Target>
          <Menu.Sub.Item>{node.label}</Menu.Sub.Item>
        </Menu.Sub.Target>
        <Menu.Sub.Dropdown>{node.children.map(renderNode)}</Menu.Sub.Dropdown>
      </Menu.Sub>
    );
  }
  return <Menu.Item key={node.label}>{node.label}</Menu.Item>;
}

function Demo() {
  const [query, setQuery] = useState('');
  const items = filterTree(data, query);

  return (
    <Menu shadow="md" width={240}>
      <Menu.Target>
        <Button>Toggle menu</Button>
      </Menu.Target>

      <Menu.Dropdown>
        <Menu.Search
          value={query}
          onChange={(event) => setQuery(event.currentTarget.value)}
          placeholder="Search items"
        />

        {items.length > 0 ? (
          items.map(renderNode)
        ) : (
          <Text c="dimmed" size="sm" ta="center" py="xs">
            Nothing found
          </Text>
        )}
      </Menu.Dropdown>
    </Menu>
  );
}

Checkbox items

Menu.CheckboxItem renders a menu item with a check indicator. It works like a regular Checkbox – manage state with checked/onChange or use defaultChecked for an uncontrolled value. By default, clicking a checkbox item does not close the menu; set closeMenuOnClick on the item (or closeOnItemClick={false} on the Menu) to override:

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

function Demo() {
  const [columns, setColumns] = useState({
    name: true,
    email: true,
    role: false,
    lastSeen: false,
  });

  const setColumn = (key: keyof typeof columns) => (checked: boolean) =>
    setColumns((current) => ({ ...current, [key]: checked }));

  return (
    <Menu shadow="md" width={220} closeOnItemClick={false}>
      <Menu.Target>
        <Button>Columns</Button>
      </Menu.Target>

      <Menu.Dropdown>
        <Menu.Label>Visible columns</Menu.Label>
        <Menu.CheckboxItem checked={columns.name} onChange={setColumn('name')}>
          Name
        </Menu.CheckboxItem>
        <Menu.CheckboxItem checked={columns.email} onChange={setColumn('email')}>
          Email
        </Menu.CheckboxItem>
        <Menu.CheckboxItem checked={columns.role} onChange={setColumn('role')}>
          Role
        </Menu.CheckboxItem>
        <Menu.CheckboxItem checked={columns.lastSeen} onChange={setColumn('lastSeen')}>
          Last seen
        </Menu.CheckboxItem>
      </Menu.Dropdown>
    </Menu>
  );
}

Checkbox group

Wrap Menu.CheckboxItem components in Menu.CheckboxGroup to manage multi-select state with a single value: string[] / onChange pair (or defaultValue for uncontrolled). Each item needs a value. Clicking an item toggles its value in the group:

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

function Demo() {
  const [columns, setColumns] = useState(['name', 'email']);

  return (
    <Menu shadow="md" width={220} closeOnItemClick={false}>
      <Menu.Target>
        <Button>Columns</Button>
      </Menu.Target>

      <Menu.Dropdown>
        <Menu.Label>Visible columns</Menu.Label>
        <Menu.CheckboxGroup value={columns} onChange={setColumns}>
          <Menu.CheckboxItem value="name">Name</Menu.CheckboxItem>
          <Menu.CheckboxItem value="email">Email</Menu.CheckboxItem>
          <Menu.CheckboxItem value="role">Role</Menu.CheckboxItem>
          <Menu.CheckboxItem value="lastSeen">Last seen</Menu.CheckboxItem>
        </Menu.CheckboxGroup>
      </Menu.Dropdown>
    </Menu>
  );
}

Menu.CheckboxItem can still be used standalone (without a group) with its own checked / defaultChecked / onChange props. Item-level checked and onChange also override the group when both are present.

Radio items

Menu.RadioItem represents a single option inside a Menu.RadioGroup. The group manages the selected value via value/onChange (or defaultValue for uncontrolled). The currently selected item displays an indicator dot. Like checkbox items, radio items do not close the menu on click by default:

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

function Demo() {
  const [sort, setSort] = useState('newest');

  return (
    <Menu shadow="md" width={220} closeOnItemClick={false}>
      <Menu.Target>
        <Button>Sort by</Button>
      </Menu.Target>

      <Menu.Dropdown>
        <Menu.Label>Order</Menu.Label>
        <Menu.RadioGroup value={sort} onChange={setSort}>
          <Menu.RadioItem value="newest">Newest first</Menu.RadioItem>
          <Menu.RadioItem value="oldest">Oldest first</Menu.RadioItem>
          <Menu.RadioItem value="popular">Most popular</Menu.RadioItem>
          <Menu.RadioItem value="commented">Most commented</Menu.RadioItem>
        </Menu.RadioGroup>
      </Menu.Dropdown>
    </Menu>
  );
}

Aligning labels of items with and without indicators

Use the alignItemsLabels prop on Menu to control how indicator slot space is reserved. This is useful when mixing Menu.Item with Menu.CheckboxItem or Menu.RadioItem and you want labels to start at the same horizontal position:

  • alignItemsLabels="with-indicators" (default) – reserves indicator space on Menu.CheckboxItem and Menu.RadioItem only. Regular Menu.Item is not padded.
  • alignItemsLabels="all" – reserves indicator space on every Menu.Item, so labels of plain items align with checkbox and radio items.
  • alignItemsLabels="none" – reserves indicator space only on items that currently show an indicator. Unchecked checkbox and radio items render without the slot (layout shifts when toggled).
import { Menu } from '@mantine/core';

function Demo() {
  return (
    <Menu>
      <Menu.Target>
        <button type="button">Toggle menu</button>
      </Menu.Target>
      <Menu.Dropdown>
        <Menu.Item>View details</Menu.Item>
        <Menu.Item>Duplicate</Menu.Item>
        <Menu.Divider />
        <Menu.CheckboxItem defaultChecked>Pinned</Menu.CheckboxItem>
        <Menu.CheckboxItem>Archived</Menu.CheckboxItem>
        <Menu.Divider />
        <Menu.RadioGroup defaultValue="newest">
          <Menu.RadioItem value="newest">Newest first</Menu.RadioItem>
          <Menu.RadioItem value="oldest">Oldest first</Menu.RadioItem>
        </Menu.RadioGroup>
      </Menu.Dropdown>
    </Menu>
  );
}

Custom check icon

Use the checkIcon prop to replace the default indicator rendered by Menu.CheckboxItem and Menu.RadioItem. Setting checkIcon on Menu applies to all checkbox/radio items in the dropdown. Setting checkIcon on an individual item overrides the menu-level value:

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

function Demo() {
  const [filters, setFilters] = useState({ open: true, drafts: false, archived: false });

  const setFilter = (key: keyof typeof filters) => (checked: boolean) =>
    setFilters((current) => ({ ...current, [key]: checked }));

  return (
    <Menu shadow="md" width={220} closeOnItemClick={false} checkIcon={<CheckIcon size={12} weight="bold" />}>
      <Menu.Target>
        <Button>Filters</Button>
      </Menu.Target>

      <Menu.Dropdown>
        <Menu.Label>Filters</Menu.Label>
        <Menu.CheckboxItem checked={filters.open} onChange={setFilter('open')}>
          Open
        </Menu.CheckboxItem>
        <Menu.CheckboxItem checked={filters.drafts} onChange={setFilter('drafts')}>
          Drafts
        </Menu.CheckboxItem>
        <Menu.CheckboxItem
          checked={filters.archived}
          onChange={setFilter('archived')}
          checkIcon="✦"
        >
          Archived
        </Menu.CheckboxItem>
      </Menu.Dropdown>
    </Menu>
  );
}

Type-ahead navigation

When focus is inside the dropdown and Menu.Search is not used, pressing a printable character key moves focus to the next menu item whose label starts with the typed character. Pressing the same character again cycles through items that start with it. Multiple characters typed in quick succession (within 500ms) match items whose labels start with the full typed string. Disabled items are skipped.

Context menu

Use Menu.ContextMenu to open the menu dropdown at the cursor position on right-click. It replaces Menu.Target and wraps the element that should respond to the contextmenu event – the browser's default context menu is suppressed, and the Mantine Menu.Dropdown is positioned at the cursor instead. Right-clicking again repositions the dropdown to the new coordinates. Set disabled to restore the browser's default context menu:

Right-click anywhere inside this area

The menu will open at the cursor position

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

function Demo() {
  return (
    <Menu shadow="md" width={200}>
      <Menu.ContextMenu>
        <Paper withBorder p="xl" radius="md" style={{ userSelect: 'none', textAlign: 'center' }}>
          <Text fw={500}>Right-click anywhere inside this area</Text>
          <Text c="dimmed" size="sm" mt={4}>
            The menu will open at the cursor position
          </Text>
        </Paper>
      </Menu.ContextMenu>

      <Menu.Dropdown>
        <Menu.Label>Actions</Menu.Label>
        <Menu.Item>Open</Menu.Item>
        <Menu.Item>Rename</Menu.Item>
        <Menu.Item>Duplicate</Menu.Item>
        <Menu.Divider />
        <Menu.Item color="red">Delete</Menu.Item>
      </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
ArrowUp/ArrowDownMoves highlight to previous/next menu item without leaving the inputFocus on Menu.Search
EnterTriggers the highlighted itemFocus on Menu.Search
Printable characterMoves focus to the next item whose label starts with the typed character. Pressing the same character cycles through matches. Multiple characters typed within 500ms match items whose labels start with the typed string.Focus within dropdown, no Menu.Search

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