# 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) => (
{key} `${v}%`} value={value[key]} onChange={(v) => update({ [key]: v })} thumbLabel={key} />
))}
); } export function InteractiveContentExample() { const { showContextMenu } = useContextMenu(); const [adjustments, setAdjustments] = useState(DEFAULT_ADJUSTMENTS); const { brightness, contrast, saturation } = adjustments; return ( ( ))} /> ); } ``` ## Example: Custom styling Source: https://icflorescu.github.io/mantine-contextmenu/examples/custom-styling/ The `ContextMenuProvider` component accepts `className`, `style`, `classNames` and `styles` props (similar to the Mantine Styles API). `className` and `style` target the component root, while `classNames` and `styles` target the individual parts: `root`, `item` and `divider`. Values set on the provider apply to all context menus rendered within it: `ProviderPropsExample.tsx`: ```tsx import { ContextMenuProvider } from 'mantine-contextmenu'; import { MantineProvider } from '@mantine/core'; import classes from './ProviderPropsExample.module.css'; export default function ProviderPropsExample({ children }: React.PropsWithChildren) { return ( {children} ); } ``` `ProviderPropsExample.module.css`: ```css .contextMenuRoot { border: 1px solid var(--mantine-color-red-6); } .contextMenuItem { background: light-dark( var(--mantine-color-red-2), var(--mantine-color-red-6) ); &:hover { background: light-dark( var(--mantine-color-red-3), var(--mantine-color-red-8) ); } } ``` When styling with class names, you may need to increase selector specificity to override the default styles (use CSS layers or repeated class names). The same properties can be set in the options object passed as the second argument to `showContextMenu`, overriding the provider values for individual context menus. The examples below use this approach. ### Styling the container with a className `StylingWithClassNameExample.tsx`: ```tsx 'use client'; import { useContextMenu } from 'mantine-contextmenu'; import { IconCopy, IconDownload } from '@tabler/icons-react'; import { Picture } from '~/components/Picture'; import { copyImageToClipboard, downloadImage, unsplashImages } from '~/lib/images'; import classes from './StylingWithClassNameExample.module.css'; export function StylingWithClassNameExample() { const { showContextMenu } = useContextMenu(); // ... return ( ); } ``` `StylingWithClassNameExample.module.css`: ```css .contextMenu { padding: var(--mantine-spacing-md); border: 1px solid var(--mantine-color-red-6); } ``` ### Styling the container with a style object or function `StylingWithStyleObjectExample.tsx`: ```tsx export function StylingWithStyleObjectExample() { const { showContextMenu } = useContextMenu(); // ... return ( ); } ``` The `style` property can also be a function receiving the current theme and returning a style object: `StylingWithStyleFunctionExample.tsx`: ```tsx export function StylingWithStyleFunctionExample() { const { showContextMenu } = useContextMenu(); // ... return ( ({ border: `1px dashed ${theme.colors.grape[6]}` }) } )} /> ); } ``` ### Styling with multiple class names `StylingWithClassNamesExample.tsx`: ```tsx 'use client'; import { useContextMenu } from 'mantine-contextmenu'; import { IconCopy, IconDownload } from '@tabler/icons-react'; import { Picture } from '~/components/Picture'; import { copyImageToClipboard, downloadImage, unsplashImages } from '~/lib/images'; import classes from './StylingWithClassNamesExample.module.css'; export default function StylingWithClassNamesExample() { const { showContextMenu } = useContextMenu(); // ... return ( ); } ``` `StylingWithClassNamesExample.module.css`: ```css .contextMenuRoot { border: 1px solid var(--mantine-color-red-6); } .contextMenuItem { background: light-dark( var(--mantine-color-red-2), var(--mantine-color-red-9) ); &:hover { background: light-dark( var(--mantine-color-red-3), var(--mantine-color-red-8) ); } } ``` ### Styling with multiple style objects or functions `StylingWithStylesObjectExample.tsx`: ```tsx export function StylingWithStylesObjectExample() { const { showContextMenu } = useContextMenu(); // ... return ( ); } ``` `StylingWithStylesFunctionsExample.tsx`: ```tsx export function StylingWithStylesFunctionsExample() { const { showContextMenu } = useContextMenu(); // ... return ( ({ border: `1px solid ${theme.colors.indigo[6]}` }), item: (theme) => ({ color: theme.colors.orange[6] }), }, } )} /> ); } ``` ### Styling individual actions Pass a `className` or `style` property to the object describing an individual menu item: `IndividualActionStylingExample.tsx`: ```tsx 'use client'; import { useContextMenu } from 'mantine-contextmenu'; 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 classes from './IndividualActionStylingExample.module.css'; export default function IndividualActionStylingExample() { const { showContextMenu } = useContextMenu(); // ... return ( , onClick: () => copyImageToClipboard(src), className: classes.copyAction, // πŸ‘ˆ action styled with className }, { key: 'download', icon: , onClick: () => downloadImage(src), // πŸ‘‡ action styled with a style object style: { fontStyle: 'italic', color: '#0d8527', }, }, { key: 'div-1', // πŸ‘‡ divider styled with a style function style: (theme) => ({ background: theme.colors.orange[6] }), }, { key: 'flipVertical', icon: , onClick: () => setFlipVertical((v) => !v), }, { key: 'flipHorizontal', icon: , onClick: () => setFlipHorizontal((v) => !v), }, ], { // πŸ‘‡ menu styled with class names classNames: { root: classes.contextMenuRoot, item: classes.contextMenuItem, }, } )} /> ); } ``` `IndividualActionStylingExample.module.css`: ```css .contextMenuRoot { border: 1px solid var(--mantine-color-orange-6); } .contextMenuItem { color: var(--mantine-color-grape-6); } .copyAction { font-weight: 700; color: var(--mantine-color-red-6); } ``` ## Example: Submenus Source: https://icflorescu.github.io/mantine-contextmenu/examples/submenus/ An item with an `items` property renders a submenu, shown when hovering over the item (or when clicking it, or pressing ArrowRight/Enter/Space when it is focused): ```tsx const { showContextMenu } = useContextMenu(); const [flipVertical, setFlipVertical] = useState(false); const [flipHorizontal, setFlipHorizontal] = useState(false); // ... return ( , key: 'copy', onClick: () => copyImageToClipboard(src), }, { icon: , key: 'download', onClick: () => downloadImage(src), }, { key: 'author', icon: , items: [ { key: 'open-in-new-tab', onClick: () => window.open(`http://unsplash.com/@${image.author.profile}`, '_blank'), }, { key: 'another-item', onClick: () => console.log('another item clicked'), }, ], }, { key: 'flip', icon: , items: [ { key: 'flipVertical', icon: , onClick: () => setFlipVertical((v) => !v), }, { key: 'flipHorizontal', icon: , onClick: () => setFlipHorizontal((v) => !v), }, ], }, ])} /> ); ``` ### Nesting Submenus can be nested as deeply as you want, though too many levels can be confusing for users: ```tsx const { showContextMenu } = useContextMenu(); // ... return ( , key: 'copy', onClick: () => copyImageToClipboard(src), }, { icon: , key: 'download', onClick: () => downloadImage(src), }, { key: 'author', icon: , items: [ { key: 'open-in-new-tab', onClick: () => window.open(`http://unsplash.com/@${image.author.profile}`, '_blank'), }, { key: 'nested-items', items: [ { key: 'nested-item-1', onClick: () => console.log('nested item 1 clicked'), }, { key: 'nested-item-2', onClick: () => console.log('nested item 2 clicked'), }, { key: 'deeply-nested-items', items: [ { key: 'deeply-nested-item-1', onClick: () => console.log('deeply nested item 1 clicked'), }, { key: 'deeply-nested-item-2', onClick: () => console.log('deeply nested item 2 clicked'), }, ], }, ], }, { key: 'more-nested-items', items: [ { key: 'nested-item-1', onClick: () => console.log('nested item 1 clicked'), }, { key: 'nested-item-2', onClick: () => console.log('nested item 2 clicked'), }, ], }, ], }, ])} /> ); ``` ### Controlling the show/hide delay Change the default `500ms` delay between hovering over or out of an item and the submenu appearing or disappearing with the `submenuDelay` prop of `ContextMenuProvider`: `SubmenuDelayProviderPropExample.tsx`: ```tsx return {children}; ``` ### Using a custom arrow icon Replace the default submenu arrow by setting the `iconRight` property: ```tsx const { showContextMenu } = useContextMenu(); const [flipVertical, setFlipVertical] = useState(false); const [flipHorizontal, setFlipHorizontal] = useState(false); // ... return ( , // πŸ‘‡ custom arrow icon iconRight: , items: [ // some nested items... ], }, { key: 'flip', icon: , // πŸ‘‡ custom arrow icon iconRight: , items: [ // more nested items... ], }, ])} /> ); ``` ## Example: Imperative hiding Source: https://icflorescu.github.io/mantine-contextmenu/examples/imperative-hiding/ A visible context menu hides automatically when the user clicks anywhere on the page, presses `Escape`, scrolls or resizes the browser window. You can also hide it imperatively by calling the `hideContextMenu` function returned by `useContextMenu`, which also returns an `isContextMenuVisible` boolean. The example below hides the menu when the user presses `H`, when the mouse cursor leaves the page, or after five seconds: `ImperativeHidingExample.tsx`: ```tsx import { useHotkeys, usePageLeave, useTimeout } from '@mantine/hooks'; import { useEffect } from 'react'; import { Picture } from '~/components/Picture'; import { copyImageToClipboard, downloadImage, unsplashImages } from '~/lib/images'; export function ImperativeHidingExample() { const { showContextMenu, hideContextMenu, isContextMenuVisible } = useContextMenu(); // πŸ‘‡ hide the context menu after five seconds have elapsed const { start: startHiding, clear: cancelHiding } = useTimeout(hideContextMenu, 5000); useEffect(() => { if (isContextMenuVisible) { startHiding(); } else { cancelHiding(); } }, [cancelHiding, isContextMenuVisible, startHiding]); // πŸ‘‡ hide the context menu when the user hits the `H` key useHotkeys([['H', hideContextMenu]]); // πŸ‘‡ hide the context menu when the mouse cursor leaves the page usePageLeave(hideContextMenu); // ... return ( copyImageToClipboard(src), }, { key: 'download', onClick: () => downloadImage(src), }, ])} /> ); } ``` ## Example: RTL support Source: https://icflorescu.github.io/mantine-contextmenu/examples/rtl-support/ Mantine ContextMenu fully supports RTL (right-to-left) layouts. It detects the text direction with Mantine’s `useDirection` hook when wrapped in Mantine’s `DirectionProvider` (see https://mantine.dev/core/direction-provider/) and adjusts automatically: - **Menu positioning**: the menu appears to the left of the cursor, staying within the viewport; - **Text alignment**: text and content are aligned to the right; - **Icon spacing**: icons are placed on the correct side using logical CSS properties; - **Submenus**: submenus open to the left of their parent items, and the keyboard arrow keys are reversed. `RtlProviderExample.tsx`: ```tsx 'use client'; import { ContextMenuProvider } from 'mantine-contextmenu'; import { DirectionProvider, Group, Paper, Switch, Text } from '@mantine/core'; import { useState } from 'react'; export function RtlProviderExample({ children }: { children: React.ReactNode }) { const [direction, setDirection] = useState<'ltr' | 'rtl'>('ltr'); return ( <>
Direction: {direction.toUpperCase()} Toggle to test RTL support
setDirection(event.currentTarget.checked ? 'rtl' : 'ltr')} label="RTL Mode" size="md" />
{children}
); } ``` `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).