ContextMenu

Displays a menu located at the pointer, triggered by a right-click or a long-press.

/* eslint-disable no-console */
import React from 'react';
import {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContentColors,
} from '@minddrop/ui';
export const ContextMenuDemo = () => (
<div style={{ display: 'flex', alignItems: 'center' }}>
<ContextMenu>
<ContextMenuTrigger>
<div style={{ border: '2px dashed white', borderRadius: 4, userSelect: 'none', padding: '45px 0', width: 300, textAlign: 'center', color: 'white', }} >
Right click here.
</div>
</ContextMenuTrigger>
<ContextMenuContent
style={{ width: 240 }}
content={[
{
type: 'menu-item',
label: 'Add title',
icon: 'title',
onSelect: () => console.log('Add title'),
keyboardShortcut: ['Ctrl', 'T'],
},
{
type: 'menu-item',
label: 'Add note',
icon: 'note',
onSelect: () => console.log('Add note'),
keyboardShortcut: ['Ctrl', 'Shift', 'N'],
},
{
type: 'menu-item',
label: 'Color',
icon: 'color-palette',
submenuContentClass: 'color-selection-submenu',
submenu: ContentColors.map((color) => ({
type: 'menu-color-selection-item',
color: color.value,
onSelect: () => console.log(color.value),
})),
},
{
type: 'menu-item',
label: 'Turn into',
icon: 'turn-into',
submenu: [
{
type: 'menu-item',
label: 'Text',
onSelect: () => console.log('Turn into text'),
},
{
type: 'menu-item',
label: 'Image',
onSelect: () => console.log('Turn into image'),
},
{
type: 'menu-item',
label: 'Equation',
onSelect: () => console.log('Turn into equation'),
},
],
},
{
type: 'menu-separator',
},
{
type: 'menu-label',
label: 'Actions',
},
{
type: 'menu-item',
label: 'Copy link',
icon: 'link',
onSelect: () => console.log('Copy link'),
keyboardShortcut: ['Ctrl', 'Shift', 'C'],
tooltipTitle: 'Copy drop link',
tooltipDescription:
'Paste the link into other drops to create a network or related information.',
},
{
type: 'menu-item',
label: 'Move to',
icon: 'arrow-up-right',
onSelect: () => console.log('Move to'),
submenuContentClass: 'topic-selection-submenu',
submenu: [
{
type: 'menu-topic-selection-item',
label: 'Sailing',
onSelect: () => console.log("Move to 'Sailing'"),
subtopics: [
{
type: 'menu-topic-selection-item',
label: 'Navigation',
onSelect: () => console.log("Move to 'Navigation'"),
subtopics: [
{
type: 'menu-topic-selection-item',
label: 'Coastal navigation',
onSelect: () =>
console.log("Move to 'Coastal navigation'"),
subtopics: [],
},
{
type: 'menu-topic-selection-item',
label: 'Offshore navigation',
onSelect: () =>
console.log("Move to 'Offshore navigation'"),
subtopics: [],
},
],
},
{
type: 'menu-topic-selection-item',
id: 'anchoring',
label: 'Anchoring',
onSelect: () => console.log("Move to 'Anchoring'"),
subtopics: [],
},
{
type: 'menu-topic-selection-item',
label: 'Sailboats',
onSelect: () => console.log("Move to 'Sailboats'"),
subtopics: [],
},
],
},
{
type: 'menu-topic-selection-item',
label: 'Home',
onSelect: () => console.log("Move to 'Home'"),
subtopics: [],
},
{
type: 'menu-topic-selection-item',
label: 'Tea',
onSelect: () => console.log("Move to 'Tea'"),
subtopics: [],
},
{
type: 'menu-topic-selection-item',
label: 'Work',
onSelect: () => console.log("Move to 'work'"),
subtopics: [],
},
{
type: 'menu-topic-selection-item',
label: 'Japanese',
onSelect: () => console.log("Move to 'Japanese'"),
subtopics: [],
},
],
},
{
type: 'menu-item',
label: 'Delete',
icon: 'trash',
onSelect: () => console.log('Delete'),
keyboardShortcut: ['Del'],
secondaryLabel: 'Delete everywhere',
secondaryOnSelect: () => console.log('Delete everywhere'),
tooltipTitle: 'Delete drop',
tooltipDescription: 'Shift + Click to delete from all topics',
},
]}
/>
</ContextMenu>
</div>
);
export default ContextMenuDemo;

Features

  • Can be generated from an array of item config objects.
  • Supports submenus with configurable reading direction.
  • Supports items, labels, separators.
  • Supports modal and non-modal modes.
  • Customize side, alignment, offsets, collision handling.
  • Focus is fully managed.
  • Full keyboard navigation.
  • Typeahead support.
  • Secondary action support when holidng Shift key
  • Triggers with a long-press on touch devices

You can compose a menu using the available components.

import {
IconButton,
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
ContextMenuLabel,
ContextMenuSeparator,
ContextMenuItem,
ContextMenuTriggerItem,
ContextMenuTopicSelectionItem,
ContextMenuColorSelectionItem,
ContentColors,
} from '@minddrop/ui';
export default () => {
return (
<ContextMenu>
<ContextMenuTrigger>
<Drop />
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem
label="Add title"
icon="title"
keyboardShortcut={['Ctrl', 'T']}
onSelect={() => console.log('Add title')}
/>
<ContextMenuItem
label="Add note"
icon="note"
keyboardShortcut={['Ctrl', 'Shift', 'N']}
onSelect={() => console.log('Add note')}
/>
<ContextMenu>
<ContextMenuTriggerItem
label="Color"
icon="color-palette"
/>
<ContextMenuContent className="color-selection-submenu">
{ContentColors.map((color) => (
<ContextMenuColorSelectionItem
key={color.value}
color={color.value}
onSelect={() => console.log(color.value)}
/>
))}
</ContextMenuContent>
</ContextMenu>
<ContextMenu>
<ContextMenuTriggerItem
label="Turn into"
icon="turn-into"
/>
<ContextMenuContent>
<ContextMenuItem
label="Text"
onSelect={() => console.log('Turn into text')}
/>
<ContextMenuItem
label="Image"
onSelect={() => console.log('Turn into image')}
/>
<ContextMenuItem
label="Equation"
onSelect={() => console.log('Turn into equation')}
/>
</ContextMenuContent>
</ContextMenu>
<ContextMenuSeparator />
<ContextMenuLabel>Actions</ContextMenuLabel>
<ContextMenuItem
label="Copy link"
icon="link"
keyboardShortcut={['Ctrl', 'Shift', 'C']}
onSelect={() => console.log('Copy link')}
tooltipTitle="Copy drop link"
tooltipDescription="Paste the link into other drops to create a network or related information."
/>
<ContextMenu>
<ContextMenuTriggerItem
label="Move to"
icon="arrow-up-right"
/>
<ContextMenuContent className="topic-selection-submenu">
<ContextMenuTopicSelectionItem
label="Sailing"
onSelect={() => console.log("Move to 'Sailing'")}
>
<ContextMenuTopicSelectionItem
label="Navigation"
onSelect={() => console.log("Move to 'Navigation'")}
>
// ...
</ContextMenuTopicSelectionItem>
// ...
</ContextMenuTopicSelectionItem>
// ...
</ContextMenuContent>
</ContextMenu>
<ContextMenuItem
label="Delete"
icon="trash"
keyboardShortcut={['Del']}
onSelect={() => console.log('Delete')}
tooltipTitle="Delete drop"
tooltipDescription="Shift + Click to delete from all topics"
/>
</ContextMenuContent>
</ContextMenu>
);
};

Context menus can also be generated by passing an array of item configs as the ContextMenuContent's content prop. The generated menu below is equivalent to the composed example above.

See the menu component configs section for details on the different configuration objects.

import {
IconButton,
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
MenuContent,
ContentColors,
} from '@minddrop/ui';
const menu: MenuContent = [
{
type: 'menu-item',
label: 'Add title',
icon: 'title',
onSelect: () => console.log('Add title'),
keyboardShortcut: ['Ctrl', 'T'],
},
{
type: 'menu-item',
label: 'Add note',
icon: 'note',
onSelect: () => console.log('Add note'),
keyboardShortcut: ['Ctrl', 'Shift', 'N'],
},
{
type: 'menu-item',
label: 'Color',
icon: 'color-palette',
submenuContentClass: 'color-selection-submenu',
submenu: ContentColors.map((color) => ({
type: 'menu-color-selection-item',
color: color.value,
onSelect: () => console.log(color.value),
})),
},
{
type: 'menu-item',
label: 'Turn into',
icon: 'turn-into',
submenu: [
{
type: 'menu-item',
label: 'Text',
onSelect: () => console.log('Turn into text'),
},
{
type: 'menu-item',
label: 'Image',
onSelect: () => console.log('Turn into image'),
},
{
type: 'menu-item',
label: 'Equation',
onSelect: () => console.log('Turn into equation'),
},
],
},
{
type: 'menu-separator',
},
{
type: 'menu-label',
label: 'Actions',
},
{
type: 'menu-item',
label: 'Copy link',
icon: 'link',
onSelect: () => console.log('Copy link'),
keyboardShortcut: ['Ctrl', 'Shift', 'C'],
tooltipTitle: 'Copy drop link',
tooltipDescription:
'Paste the link into other drops to create a network or related information.',
},
{
type: 'menu-item',
label: 'Move to',
icon: 'arrow-up-right',
submenuContentClass: 'topic-selection-submenu',
onSelect: () => console.log('Move to'),
submenu: [
{
id: 'sailing',
label: 'Sailing',
onSelect: () => console.log("Move to 'Sailing'"),
subtopics: [
{
id: 'navigation',
label: 'Navigation',
onSelect: () => console.log("Move to 'Navigation'"),
subtopics: [
// ...
],
},
// ...
],
},
// ...
],
},
{
type: 'menu-item',
label: 'Delete',
icon: 'trash',
onSelect: () => console.log('Delete'),
keyboardShortcut: ['Del'],
secondaryLabel: 'Delete everywhere',
secondaryOnSelect: () => console.log('Delete everywhere'),
tooltipTitle: 'Delete drop',
tooltipDescription: 'Shift + Click to delete from all topics',
},
];
export default () => {
return (
<ContextMenu>
<ContextMenuTrigger>
<IconButton icon="more-vertical" label="Drop options" />
</ContextMenuTrigger>
<ContextMenuContent content={menu} />
</ContextMenu>
);
};

Contains all the parts of a context menu.

PropTypeDefault
defaultOpenbooleanNo default value
openbooleanNo default value
onOpenChangefunctionNo default value
modalbooleantrue
direnum"ltr"
idstringNo default value

The area that opens the context menu. Wrap it around the target you want the context menu to open from when right-clicking (or using the relevant keyboard shortcuts).

PropTypeDefault
asChildbooleantrue

The component that pops out in an open context menu.

PropTypeDefault
childrennodeNo default value
contentarrayNo default value
allowPinchZoombooleanfalse
loopbooleanfalse
onCloseAutoFocusfunctionNo default value
onEscapeKeyDownfunctionNo default value
onPointerDownOutsidefunctionNo default value
onFocusOutsidefunctionNo default value
onInteractOutsidefunctionNo default value
portalledbooleantrue
forceMountbooleanNo default value
sideenum"bottom"
sideOffsetnumber0
alignenum"center"
alignOffsetnumber0
avoidCollisionsbooleantrue
collisionTolerancenumber0

An item in the menu. Items can be selected using the mouse or keyboard. Items can be given a secondary action which is enabled when the Shift key is held down.

PropTypeDefault
label*nodeNo default value
onSelect*functionNo default value
iconIconName | ReactElementNo default value
keyboardShortcutstring[]No default value
secondaryLabelnodeNo default value
secondaryOnSelectfunctionNo default value
secondaryIconIconName | ReactElementNo default value
secondaryKeyboardShortcutstring[]No default value
tooltipTitlenodeNo default value
tooltipDescriptionnodeNo default value
disabledbooleanNo default value
textValuestringNo default value

An item that opens a submenu. Used in combination with a nested ContextMenu. Must be rendered inside a root ContextMenu. Has the same props API as a regular ContextMenuItem.

PropTypeDefault
label*nodeNo default value
onSelectfunctionNo default value
iconIconName | ReactElementNo default value
keyboardShortcutstring[]No default value
tooltipTitlenodeNo default value
tooltipDescriptionnodeNo default value
disabledbooleanNo default value
textValuestringNo default value

A menu item designed for selecting a topic. Can be expanded to reveal subtopics.

PropTypeDefault
label*stringNo default value
onSelectfunctionNo default value
childrenReact.ComponentType<ContextMenuTopicSelectionItemProps>No default value

A menu item designed for selecting a ContentColor.

PropTypeDefault
color*ContentColorNo default value
onSelectfunctionNo default value

Used to render a label. It won't be focusable using arrow keys.

PropTypeDefault
children*nodeNo default value

Used to visually separate items in the context menu. Does not take any props.

Context menus can also be generated by passing an array of menu component configuration objects and React components as the ContextMenuContent's content prop.

To create a menu label, add a MenuLabelConfig config object to the menu array.

const menu = [
// ...
{
type: 'menu-label',
label: 'Actions',
},
// ...
];
PropTypeDefault
type*menu-labelNo default value
label*stringNo default value

To create a horizontal separator, add a MenuSeparatorConfig object to the menu array.

const menu = [
// ...
{
type: 'menu-separator',
},
// ...
];
PropTypeDefault
type*menu-separatorNo default value

To create a menu item, add a MenuItemConfig object to the menu array.

const menu = [
// ...
{
type: 'menu-item',
label: 'Copy link',
icon: 'link',
onSelect: () => console.log('Copy link'),
keyboardShortcut: ['Ctrl', 'Shift', 'C'],
tooltipTitle: 'Copy drop link',
tooltipDescription:
'Paste the link into other drops to create a network or related information.',
},
// With submenu
{
type: 'menu-item',
label: 'Turn into',
icon: 'turn-into',
submenu: [
{
type: 'menu-item',
label: 'Text',
onSelect: () => console.log('Turn into text'),
},
{
type: 'menu-item',
label: 'Image',
onSelect: () => console.log('Turn into image'),
},
{
type: 'menu-item',
label: 'Equation',
onSelect: () => console.log('Turn into equation'),
},
],
},
// With secondary action
{
type: 'menu-item',
label: 'Delete',
icon: 'trash',
onSelect: () => console.log('Delete'),
keyboardShortcut: ['Del'],
secondaryLabel: 'Delete everywhere',
secondaryOnSelect: () => console.log('Delete everywhere'),
tooltipTitle: 'Delete drop',
tooltipDescription: 'Shift + Click to delete from all topics',
},
// ...
];
PropTypeDefault
type*menu-itemNo default value
label*nodeNo default value
iconIconName | ReactElementNo default value
onSelect*functionNo default value
keyboardShortcutstring[]No default value
secondaryLabelnodeNo default value
secondaryOnSelectfunctionNo default value
secondaryIconIconName | ReactElementNo default value
secondaryKeyboardShortcutstring[]No default value
tooltipTitlenodeNo default value
tooltipDescriptionnodeNo default value
submenuarrayNo default value
disabledbooleanNo default value

To create a topic selection menu item, add a MenuTopicSelectionItemConfig object to the menu array.

const menu = [
// ...
{
type: 'menu-topic-selection-item',
label: 'Sailing',
onSelect: () => console.log("Move to 'Sailing'"),
subtopics: [
{
type: 'menu-topic-selection-item',
label: 'Sailing',
onSelect: () => console.log("Move to 'Sailing'"),
subtopics: [],
},
],
},
// ...
];
PropTypeDefault
type*menu-topic-selection-itemNo default value
title*stringNo default value
id*stringNo default value
subtopics*TopicSelectionItem[]No default value
onSelectfunctionNo default value

To create a color selection menu item, add a MenuColorSelectionItemConfig object to the menu array.

const menu = [
// ...
{
type: 'menu-color-selection-item',
color: 'green',
onSelect: () => console.log('green'),
},
// ...
];
PropTypeDefault
type*menu-color-selection-itemNo default value
color*ContentColorNo default value
onSelectfunctionNo default value

Radix exposes a CSS custom property --radix-context-menu-content-transform-origin. Use it to animate the content from its computed origin based on side, sideOffset, align, alignOffset and any collisions.

@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0);
}
to {
opacity: 1;
transform: scale(1);
}
}
.context-menu {
transform-origin: var(
--radix-context-menu-content-transform-origin
);
animation: scaleIn 0.5s ease-out;
}

Radix exposes data-side and data-align attributes. Their values will change at runtime to reflect collisions. Use them to create collision and direction-aware animations.

@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(0);
}
to {
opacity: 1;
transform: translateY(-10px);
}
}
.context-menu {
animation-duration: 0.6s;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
.context-menu[data-side='bottom'] {
animation: slideDown;
}
.context-menu[data-side='top'] {
animation: slideUp;
}

Adheres to the Menu WAI-ARIA design pattern and uses roving tabindex to manage focus movement among menu items.

KeyDescription
SpaceActivates the focused item.
EnterActivates the focused item.
ArrowDownMoves focus to the next item.
ArrowUpMoves focus to the previous item.
ArrowRightArrowLeft When focus is on DropdownMenuTriggerItem, opens or closes the submenu depending on reading direction. When focus is on DropdownMenuTopicSelectionItem, expands or collapses the topic's subtopics.
EscCloses the context menu