# Mantine ContextMenu
> Design your Mantine applications for productivity and meet your usersβ expectations by enhancing your UIs with desktop-grade context menus. `mantine-contextmenu` is a lightweight, dark-theme aware, accessible context-menu component for React applications built with Mantine (https://mantine.dev), supporting icons, colors, dividers, submenus, custom content, custom styling and RTL layouts.
Documentation website: https://icflorescu.github.io/mantine-contextmenu/
Repository: https://github.com/icflorescu/mantine-contextmenu
Changelog: https://github.com/icflorescu/mantine-contextmenu/blob/main/CHANGELOG.md
## Key facts
- Mantine ContextMenu V9.x is compatible with Mantine V9.x; peer dependencies are `@mantine/core`, `@mantine/hooks`, `clsx`, `react` and `react-dom`.
- Wrap your application in `ContextMenuProvider` **inside** `MantineProvider`.
- Import `mantine-contextmenu/styles.layer.css` (or `mantine-contextmenu/styles.css`) **after** the Mantine core styles; when using `styles.layer.css`, declare `@layer mantine, mantine-contextmenu;` so Mantine core styles are applied first.
- `useContextMenu()` returns `{ showContextMenu, hideContextMenu, isContextMenuVisible }`. Since it is a hook, files using it need the `'use client'` directive in React Server Components environments (e.g. Next.js app router).
- `showContextMenu(content, options?)` does not show the menu directly: it **returns an event handler**, meant to be passed to `onContextMenu` (e.g. `onContextMenu={showContextMenu([...])}`); it can also be used as a click or touch event handler.
- `content` is either an array of item objects, or a render function `(close) => JSX.Element` for custom content.
- Each item needs a unique `key`. An item with an `onClick` handler is an action, an item with an `items` array opens a submenu, and an item with neither is rendered as a divider.
- When `title` is omitted, it is generated by "humanizing" the `key` (e.g. `copyToClipboard` β "Copy to clipboard").
- Items also accept `icon`, `iconRight`, `color` (any Mantine color), `disabled`, `hidden`, `className` and `style`.
- The menu hides when an item is clicked, when clicking outside of it, on `Escape`, and when the window is scrolled or resized; it can also be hidden imperatively with `hideContextMenu()`.
- Clicking inside custom content does not close the menu, so it can contain inputs or other interactive controls; call the `close` function received by the render callback to close it.
- Keyboard support: the menu can be opened with Shift+F10 or the context menu key, in which case its first item is focused; ArrowUp/ArrowDown/Home/End move between items, ArrowRight/Enter/Space open a submenu, ArrowLeft/Escape close it (reversed in RTL), and focus returns to the triggering element when the menu closes. Items use the `menu`, `menuitem` and `separator` ARIA roles.
- RTL layouts are supported automatically through Mantineβs `DirectionProvider`.
Note: the examples use a few documentation-website helpers which are not part of the package: the `Picture` component (renders a focusable image and forwards the `onContextMenu` handler), and the `unsplashImages`, `copyImageToClipboard` and `downloadImage` utilities. Code hidden for brevity is marked with `// ...` comments.
## Getting started
Source: https://icflorescu.github.io/mantine-contextmenu/getting-started/
Mantine ContextMenu depends on `@mantine/core`, `@mantine/hooks` and `clsx`.
Create a new application with Mantine (see https://mantine.dev/getting-started/), then install `mantine-contextmenu` and make sure you have the `clsx` dependency installed as well:
```shell
npm i mantine-contextmenu clsx
# or: pnpm i mantine-contextmenu clsx
# or: yarn add mantine-contextmenu clsx
```
Wrap your application in a `ContextMenuProvider` **inside** the `MantineProvider` and import the necessary CSS files **in the correct order**: the `@mantine/core` styles must be applied before Mantine ContextMenu styles (see the Styling section below).
For example, in a Next.js application with an app router, your `layout.tsx` could look like this:
`RootLayout.tsx`:
```tsx
import { ContextMenuProvider } from 'mantine-contextmenu';
import { ColorSchemeScript, MantineProvider } from '@mantine/core';
import '@mantine/core/styles.layer.css';
import 'mantine-contextmenu/styles.layer.css';
import './layout.css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
`layout.css`:
```css
/* π Make sure the styles are applied in the correct order */
@layer mantine, mantine-contextmenu;
```
### Use the hook in your code
Import the `useContextMenu` hook and use it in your components like so:
`SimpleExample.tsx`:
```tsx
'use client';
// π Since 'useContextMenu' is a hook, don't forget to add the 'use client' directive
// at the top of your file if you're using it in a RSC context
import { useContextMenu } from 'mantine-contextmenu';
// other imports...
export function SimpleExample() {
const { showContextMenu } = useContextMenu();
// ...
return (
,
title: 'Copy to clipboard',
onClick: () => copyImageToClipboard(src),
},
{
key: 'download',
icon: ,
title: 'Download to your device',
onClick: () => downloadImage(src),
},
])}
/>
);
}
```
Right-clicking the image (or long-tapping it on touch devices) shows the context menu.
## Styling
Source: https://icflorescu.github.io/mantine-contextmenu/styling/
Since Mantine V7, styling is done with native CSS instead of CSS-in-JS (Emotion), so `createStyles` is no longer available. All `@mantine/*` packages ship native CSS files which can be imported from `@mantine/{package}/styles.css` or `@mantine/{package}/styles.layer.css`.
Similarly, Mantine ContextMenu ships native CSS files which can be imported from `mantine-contextmenu/styles.css` or `mantine-contextmenu/styles.layer.css`.
### Controlling the order of styles with CSS layers
Some bundlers and frameworks (including Next.js) do not allow you to control the order of stylesheets when importing CSS files. You can mitigate this by using CSS layers:
- `@mantine/{package}/styles.layer.css` files place styles in a layer named `mantine`;
- `mantine-contextmenu/styles.layer.css` places styles in a layer named `mantine-contextmenu`.
For example, in a Next.js application you can ensure the correct order of styles by importing the `styles.layer.css` files and using the `@layer` directive:
`RootLayout.tsx`:
```tsx
import { ContextMenuProvider } from 'mantine-contextmenu';
import { ColorSchemeScript, MantineProvider } from '@mantine/core';
// π Import the mantine-core layer CSS file;
// this will automatically place it in a `mantine` layer
import '@mantine/core/styles.layer.css';
// π Import the mantine-contextmenu layer CSS file;
// this will automatically place it in a `mantine-contextmenu` layer
import 'mantine-contextmenu/styles.layer.css';
// π Import your own CSS file;
// make sure to specify the layers order with the `@layer` directive
// inside that file
import './layout.css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
`layout.css`:
```css
/* π Make sure the styles are applied in the correct order */
@layer mantine, mantine-contextmenu;
```
For even more control, use the `postcss-import` plugin to set the layer names when importing the `styles.css` files, then use the `@layer` directive:
`RootLayout.tsx`:
```tsx
import { ContextMenuProvider } from 'mantine-contextmenu';
import { ColorSchemeScript, MantineProvider } from '@mantine/core';
import './layout.css';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
`postcss.config.js`:
```js
module.exports = {
plugins: {
// π the postcss-import plugin must come first;
// make sure to install it with `npm i -D postcss-import` or `yarn add -D postcss-import`
'postcss-import': {},
'postcss-preset-mantine': {},
'postcss-simple-vars': {
// your Mantine variables here...
},
},
};
```
`layout.css`:
```css
/* π Directly import the mantine core styles into mantine-core layer */
@import "@mantine/core/styles.css" layer(mantine-core);
/* π Directly import the mantine-contextmenu styles into mantine-contextmenu layer */
@import "mantine-contextmenu/dist/styles.css" layer(mantine-contextmenu);
/* π Make sure the styles are applied in the correct order */
@layer mantine-core, mantine-contextmenu;
```
## Example: Basic usage
Source: https://icflorescu.github.io/mantine-contextmenu/examples/basic-usage/
In its most basic usage scenario, the `showContextMenu` function only requires an array of objects describing the menu items. Each object must have a unique `key` and an `onClick` handler. A title is generated automatically by "humanizing" the `key` value, and the `onClick` handler is called when the user clicks the corresponding item.
`BasicUsageExample.tsx`:
```tsx
const { showContextMenu } = useContextMenu();
// ...
return (
copyImageToClipboard(src),
},
{
key: 'download',
onClick: () => downloadImage(src),
},
])}
/>
);
```
## Example: Basic configuration
Source: https://icflorescu.github.io/mantine-contextmenu/examples/basic-configuration/
The `ContextMenuProvider` component accepts the following props, used as default values for the context menu container:
- `zIndex: number` β defaults to `9999`;
- `shadow: MantineShadow` β defaults to `sm`;
- `borderRadius: MantineRadius` β defaults to `xs`;
- `submenuDelay: number` β delay in ms when showing and hiding submenus, defaults to `500`;
- `repositionOnRepeat: boolean` β defaults to `false`; if `true`, the menu repositions itself when the triggering event repeats, otherwise it hides.
`ProviderPropsExample.tsx`:
```tsx
return (
{children}
);
```
The `zIndex` value can be overridden through the options object passed as the second argument to `showContextMenu`:
`BasicConfigurationExampleContent.tsx`:
```tsx
const { showContextMenu } = useContextMenu();
// ...
return (
copyImageToClipboard(src),
},
{
key: 'download',
onClick: () => downloadImage(src),
},
],
{ zIndex: 3000 }
)}
/>
);
```
## Example: Action titles
Source: https://icflorescu.github.io/mantine-contextmenu/examples/action-titles/
If youβre not happy with the titles generated automatically from the item keys, provide your own with the `title` property (it accepts any React node):
`ActionTitlesExample.tsx`:
```tsx
const { showContextMenu } = useContextMenu();
// ...
return (
copyImageToClipboard(src),
},
{
key: 'download',
title: 'Download image to your computer',
onClick: () => downloadImage(src),
},
])}
/>
);
```
## Example: Action icons
Source: https://icflorescu.github.io/mantine-contextmenu/examples/action-icons/
Set the `icon` and `iconRight` properties to display icons on either side of the item title:
`ActionIconsExample.tsx`:
```tsx
const { showContextMenu } = useContextMenu();
// ...
return (
,
onClick: () => copyImageToClipboard(src),
},
{
key: 'download',
iconRight: ,
onClick: () => downloadImage(src),
},
{
key: 'delete',
icon: ,
iconRight: ,
onClick: () => handleDelete,
},
])}
/>
);
```
## Example: Action colors
Source: https://icflorescu.github.io/mantine-contextmenu/examples/action-colors/
Set the `color` property to any Mantine color to make certain actions stand out:
`ActionColorsExample.tsx`:
```tsx
const { showContextMenu } = useContextMenu();
// ...
return (
,
onClick: () => copyImageToClipboard(src),
},
{
key: 'download',
color: '#ff00ff',
icon: ,
onClick: () => downloadImage(src),
},
{
key: 'delete',
color: 'red',
icon: ,
onClick: handleDelete,
},
])}
/>
);
```
## Example: Action dividers
Source: https://icflorescu.github.io/mantine-contextmenu/examples/action-dividers/
An item missing both its `onClick` handler and its `items` property is rendered as a divider:
`ActionDividersExample.tsx`:
```tsx
const { showContextMenu } = useContextMenu();
const [flipVertical, setFlipVertical] = useState(false);
const [flipHorizontal, setFlipHorizontal] = useState(false);
// ...
return (
,
onClick: () => copyImageToClipboard(src),
},
{
key: 'download',
icon: ,
onClick: () => downloadImage(src),
},
// π render a divider
// (the key can be anything, it's just used to identify the item)
{ key: 'divider' },
{
key: 'flipVertical',
icon: ,
onClick: () => setFlipVertical((v) => !v),
},
{
key: 'flipHorizontal',
icon: ,
onClick: () => setFlipHorizontal((v) => !v),
},
])}
/>
);
```
## Example: Conditionally disabling items
Source: https://icflorescu.github.io/mantine-contextmenu/examples/conditionally-disabling-items/
Set an itemβs `disabled` property to `true` to disable it:
`ConditionallyDisablingItemsExample.tsx`:
```tsx
const { showContextMenu } = useContextMenu();
// ...
return (
copyImageToClipboard(src),
},
{
key: 'download',
// π this item is disabled; the `onClick` will not be called
disabled: true,
onClick: () => downloadImage(src),
},
])}
/>
);
```
## Example: Conditionally hiding items
Source: https://icflorescu.github.io/mantine-contextmenu/examples/conditionally-hiding-items/
Set an itemβs `hidden` property to `true` to hide it instead of disabling it:
`ConditionallyHidingItemsExample.tsx`:
```tsx
const { showContextMenu } = useContextMenu();
// ...
return (
copyImageToClipboard(src),
},
{
key: 'download',
// π do not render this item
hidden: true,
onClick: () => downloadImage(src),
},
])}
/>
);
```
## Example: Multiple targets
Source: https://icflorescu.github.io/mantine-contextmenu/examples/multiple-targets/
The context menu component tree is only created when the menu is shown and destroyed when it is hidden, so using the same `showContextMenu` function on many target elements does not cause performance issues:
`MultipleTargetsExample.tsx`:
```tsx
// π the same function will be used for multiple components
const { showContextMenu } = useContextMenu();
return (
{images.map((image, index) => (
,
onClick: () => copyImageToClipboard(image.file.src),
},
{
key: 'download',
icon: ,
onClick: () => downloadImage(image.file.src),
},
{ key: 'divider' },
{
key: 'author',
icon: ,
title: `Open ${image.author.name}βs profile`,
onClick: () => window.open(`https://unsplash.com/@${image.author.profile}`, '_blank'),
},
])}
/>
))}
);
```
## Example: Custom content
Source: https://icflorescu.github.io/mantine-contextmenu/examples/custom-content/
To display custom content, pass a render callback to `showContextMenu` instead of an array of items. The callback receives a `close` function you can use to close the menu:
`CustomContentExample.tsx`:
```tsx
'use client';
import { useContextMenu } from 'mantine-contextmenu';
import { Flex } from '@mantine/core';
import { IconCopy, IconDownload, IconFlipHorizontal, IconFlipVertical } from '@tabler/icons-react';
import { useState } from 'react';
import { Picture } from '~/components/Picture';
import { copyImageToClipboard, downloadImage, unsplashImages } from '~/lib/images';
import { FancyButton } from './FancyButton';
export function CustomContentExample() {
const { showContextMenu } = useContextMenu();
const [flipVertical, setFlipVertical] = useState(false);
const [flipHorizontal, setFlipHorizontal] = useState(false);
const image = unsplashImages[7];
const { src } = image.file;
return (
(
}
text="Copy"
onClick={() => {
close();
copyImageToClipboard(src);
}}
/>
}
text="Download"
onClick={() => {
close();
downloadImage(src);
}}
/>
}
text="Flip vertical"
onClick={() => {
close();
setFlipVertical((v) => !v);
}}
/>
}
text="Flip horizontal"
onClick={() => {
close();
setFlipHorizontal((v) => !v);
}}
/>
))}
/>
);
}
```
`FancyButton.tsx`:
```tsx
import { Box, Text } from '@mantine/core';
import classes from './FancyButton.module.css';
export type FancyButtonProps = {
icon: React.ReactNode;
text: string;
onClick: () => void;
};
export function FancyButton({ icon, text, onClick }: FancyButtonProps) {
return (
{icon}
{text}
);
}
```
`FancyButton.module.css`:
```css
.root {
width: 50%;
height: rem(60px);
padding: 0;
border-radius: 0;
cursor: pointer;
background: light-dark(
var(--mantine-color-gray-0),
var(--mantine-color-dark-5)
);
transition: background 0.2s;
border: 0;
--border: 1px solid
light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-3));
&:nth-child(2n) {
border-left: var(--border);
}
&:nth-child(n + 3) {
border-top: var(--border);
}
&:hover {
background: light-dark(rgba(0, 0, 0, 0.1), var(--mantine-color-dark-6));
}
}
```
### Interactive content
Custom content can contain interactive elements such as inputs, sliders or checkboxes: the menu is only closed when clicking outside of it, pressing `Escape`, or when you call the `close` function. The render callback is called with the values available when the menu is shown, so interactive content should keep its own state in a separate component:
`InteractiveContentExample.tsx`:
```tsx
'use client';
import { useContextMenu } from 'mantine-contextmenu';
import { Button, Group, Slider, Stack, Text } from '@mantine/core';
import { useState } from 'react';
import { Picture } from '~/components/Picture';
import { unsplashImages } from '~/lib/images';
type Adjustments = { brightness: number; contrast: number; saturation: number };
const DEFAULT_ADJUSTMENTS: Adjustments = { brightness: 100, contrast: 100, saturation: 100 };
type AdjustmentsMenuProps = {
initialValue: Adjustments;
onChange: (value: Adjustments) => void;
close: () => void;
};
// The menu content is rendered from the callback passed to `showContextMenu` when the menu is shown,
// so interactive controls should keep their own state in a component like this one
function AdjustmentsMenu({ initialValue, onChange, close }: AdjustmentsMenuProps) {
const [value, setValue] = useState(initialValue);
const update = (changes: Partial) => {
const newValue = { ...value, ...changes };
setValue(newValue);
onChange(newValue);
};
return (
{(Object.keys(DEFAULT_ADJUSTMENTS) as (keyof Adjustments)[]).map((key) => (
>
);
}
```
`RtlSupportExampleContent.tsx`:
```tsx
'use client';
import { useContextMenu } from 'mantine-contextmenu';
import {
IconClipboard,
IconCopy,
IconCut,
IconDownload,
IconEdit,
IconFileExport,
IconShare,
IconTrash,
} from '@tabler/icons-react';
import { Picture } from '~/components/Picture';
import { unsplashImages } from '~/lib/images';
export function RtlSupportExampleContent() {
const { showContextMenu } = useContextMenu();
const image = unsplashImages[0];
return (
,
title: 'Edit',
onClick: () => {},
},
{
key: 'copy',
icon: ,
title: 'Copy',
onClick: () => {},
},
{
key: 'cut',
icon: ,
title: 'Cut',
onClick: () => {},
},
{
key: 'paste',
icon: ,
title: 'Paste',
onClick: () => {},
},
{ key: 'divider-1' },
{
key: 'share',
icon: ,
title: 'Share',
items: [
{
key: 'share-email',
title: 'Email',
onClick: () => {},
},
{
key: 'share-social',
title: 'Social Media',
items: [
{ key: 'facebook', title: 'Facebook', onClick: () => {} },
{ key: 'twitter', title: 'Twitter', onClick: () => {} },
{ key: 'linkedin', title: 'LinkedIn', onClick: () => {} },
],
},
{
key: 'share-link',
title: 'Copy Link',
iconRight: ,
onClick: () => {},
},
],
},
{
key: 'export',
icon: ,
title: 'Export',
items: [
{ key: 'export-pdf', title: 'Export as PDF', onClick: () => {} },
{ key: 'export-png', title: 'Export as PNG', onClick: () => {} },
{ key: 'export-svg', title: 'Export as SVG', onClick: () => {} },
],
},
{ key: 'divider-2' },
{
key: 'download',
icon: ,
title: 'Download',
onClick: () => {},
},
{
key: 'delete',
icon: ,
title: 'Delete',
color: 'red',
onClick: () => {},
},
])}
/>
);
}
```
## Type definitions
Source: https://icflorescu.github.io/mantine-contextmenu/type-definitions/
Mantine ContextMenu is written in TypeScript and its options are documented with JSDoc annotations. Here are the exported types:
`types.ts`:
```ts
import type { MantineColor, MantineRadius, MantineShadow, MantineStyleProp, StylesRecord } from '@mantine/core';
import type { JSX } from 'react';
/**
* Utility type that makes a property required
*/
export type WithRequiredProperty = Type & {
[Property in Key]-?: Type[Property];
};
/**
* Utility type that makes a property optional
*/
export type WithOptionalProperty = Omit & Partial>;
/**
* Context menu options
*/
export type ContextMenuOptions = {
/**
* The z-index of context menu.
* @default 9999
*/
zIndex?: number;
/**
* Context menu container class name.
*/
className?: string;
/**
* Context menu container style.
*/
style?: MantineStyleProp;
/**
* Context menu elements class names.
*/
classNames?: Partial>;
/**
* Context menu styles.
* Can be an object with `root`, `item` and `divider` keys and style objects as values,
* or a function that accepts the current theme and returns a similarly structured object.
*/
styles?: StylesRecord<'root' | 'item' | 'divider', MantineStyleProp>;
};
/**
* Generic context menu settings
*/
export type ContextMenuSettings = {
/**
* Context menu shadow.
* @default 'sm'
*/
shadow?: MantineShadow;
/**
* Context menu border radius.
* @default 'xs'
*/
borderRadius?: MantineRadius;
/**
* Delay in ms to use when showing and hiding submenus.
* @default 500
*/
submenuDelay?: number;
/**
* Whether to reposition the context menu when the triggering event repeats. If set to true, the context menu will
* reposition itself to the position of the triggering event. If set to false, the context menu will hide
* automatically when the triggering event repeats.
* @default false
*/
repositionOnRepeat?: boolean;
};
/**
* Context menu provider props
*/
export type ContextMenuProviderProps = ContextMenuSettings &
ContextMenuOptions & {
children: React.ReactNode;
};
/**
* Context menu item options
*/
export type ContextMenuItemOptions = {
/**
* Unique key of the context menu item or divider.
*/
key: string;
/**
* Context menu item or divider className.
*/
className?: string;
/**
* Context menu item or divider style.
*/
style?: MantineStyleProp;
/**
* Boolean indicating whether the context menu item is hidden.
*/
hidden?: boolean;
} & (
| ({
/**
* Optional context menu item icon.
*/
icon?: React.ReactNode;
/**
* Optional context menu item icon for the right side of the title.
* If provided this will overwrite the submenu's βΆ icon
*/
iconRight?: React.ReactNode;
/**
* Optional context menu item title.
* If not provided, one will be generated automatically by "humanizing" the key.
* @default humanize(key)
*/
title?: React.ReactNode;
/**
* Optional context menu item color.
*/
color?: MantineColor;
/**
* Boolean indicating whether the context menu item is disabled.
*/
disabled?: boolean;
} & (
| {
/**
* Optional context menu item `onClick` handler.
* If not provided, a divider will be rendered instead.
*/
onClick: React.MouseEventHandler;
items?: never;
}
| {
onClick?: never;
/**
* Optional items.
* If provided, a submenu will be rendered.
*/
items: ContextMenuItemOptions[];
}
))
| {
icon?: never;
iconRight?: never;
title?: never;
color?: never;
disabled?: never;
onClick?: never;
items?: never;
}
);
/**
* Context menu content.
* Either an array of context menu items, or a function that accepts a close callback and returns context menu content.
*/
export type ContextMenuContent = ContextMenuItemOptions[] | ((close: () => void) => JSX.Element);
/**
* Show context menu function
*/
export type ShowContextMenuFunction = (
/**
* Context menu content.
* Either an array of context menu items or a function that accepts a close callback and returns context menu content.
*/
content: ContextMenuContent,
/**
* Context menu options (overrides provider props).
*/
options?: ContextMenuOptions
) => React.MouseEventHandler & React.TouchEventHandler;
/**
* Hide context menu function
*/
export type HideContextMenuFunction = () => void;
```
## Mantine V6 support
Source: https://icflorescu.github.io/mantine-contextmenu/mantine-v6-support/
Mantine ContextMenu V7+ requires Mantine V7 or newer, since Mantine V7 migrated from CSS-in-JS (Emotion) to native CSS. If youβre still using Mantine V6, use Mantine ContextMenu V6 instead (documentation: https://icflorescu.github.io/mantine-contextmenu-v6).