react-smart-contextmenu
v1.0.0
Published
A highly customizable, accessible React context menu component
Maintainers
Readme
React Smart Context Menu
A highly customizable, accessible React context menu component with keyboard navigation, themes, and smooth animations.
Demo • Installation • Usage • API • Examples
✨ Features
- 🎯 Easy to use - Simple API with comprehensive TypeScript support
- ⌨️ Full keyboard navigation - Arrow keys, Enter, Escape, Home, End support
- 🎨 Customizable themes - Built-in light/dark themes or create your own
- 📱 Smart positioning - Automatically adjusts to stay within viewport bounds
- ♿ Accessibility focused - ARIA compliant with proper focus management
- 🎭 Smooth animations - Beautiful animations powered by Framer Motion
- 🔧 Flexible options - Icons, shortcuts, separators, headers, disabled states
- 🎛️ Multiple sizes - Small, medium, and large size variants
- 🚫 Click outside detection - Automatically closes when clicking outside
- 🔄 Portal rendering - Renders outside component hierarchy to avoid z-index issues
- 📦 Tree-shakeable - Only bundle what you use
- 🔥 Zero dependencies - Except peer dependencies (React, Framer Motion)
🚀 Installation
npm install react-smart-contextmenu framer-motionyarn add react-smart-contextmenu framer-motionpnpm add react-smart-contextmenu framer-motionPeer Dependencies
This package requires the following peer dependencies:
react>= 16.8.0react-dom>= 16.8.0framer-motion>= 6.0.0
📖 Quick Start
import React from 'react';
import { useContextMenu, ContextMenu } from 'react-smart-contextmenu';
function MyComponent() {
const {
isOpen,
position,
handleContextMenu,
closeMenu,
focusedIndex,
setFocusedIndex
} = useContextMenu();
const menuOptions = [
{
label: 'Copy',
onClick: () => console.log('Copy clicked'),
shortcut: 'Ctrl+C',
icon: '📋'
},
{
label: 'Paste',
onClick: () => console.log('Paste clicked'),
shortcut: 'Ctrl+V',
icon: '📄'
},
{ type: 'separator' as const },
{
label: 'Delete',
onClick: () => console.log('Delete clicked'),
danger: true,
icon: '🗑️'
}
];
return (
<>
<div
onContextMenu={handleContextMenu}
className="p-8 bg-gray-100 rounded-lg cursor-context-menu"
>
Right-click me for context menu!
</div>
<ContextMenu
isOpen={isOpen}
position={position}
onClose={closeMenu}
options={menuOptions}
focusedIndex={focusedIndex}
setFocusedIndex={setFocusedIndex}
/>
</>
);
}🎨 Theming
Built-in Themes
// Light theme (default)
<ContextMenu theme="light" {...props} />
// Dark theme
<ContextMenu theme="dark" {...props} />Custom Theme
import { ContextMenuTheme } from 'react-smart-contextmenu';
const customTheme: ContextMenuTheme = {
background: '#2d3748',
border: '#4a5568',
text: '#e2e8f0',
textMuted: '#a0aec0',
hover: '#4a5568',
hoverDanger: '#742a2a',
danger: '#fc8181',
separator: '#4a5568',
disabled: '#718096',
shadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
focused: '#4a5568',
focusedDanger: '#9c4221'
};
<ContextMenu theme={customTheme} {...props} />📏 Sizes
// Small
<ContextMenu size="sm" {...props} />
// Medium (default)
<ContextMenu size="md" {...props} />
// Large
<ContextMenu size="lg" {...props} />🔧 Advanced Usage
With Target Element Context
function FileExplorer() {
const { isOpen, position, targetElement, handleContextMenu, closeMenu, focusedIndex, setFocusedIndex } = useContextMenu();
const files = ['document.pdf', 'image.jpg', 'video.mp4'];
const getMenuOptions = (fileName: string) => [
{
label: 'Open',
onClick: () => console.log('Opening', fileName),
icon: '📂'
},
{
label: 'Rename',
onClick: () => console.log('Renaming', fileName),
icon: '✏️'
},
{ type: 'separator' as const },
{
label: 'Delete',
onClick: () => console.log('Deleting', fileName),
danger: true,
icon: '🗑️'
}
];
return (
<>
{files.map(file => (
<div
key={file}
onContextMenu={(e) => handleContextMenu(e, file)}
className="p-2 hover:bg-gray-100 cursor-pointer"
>
{file}
</div>
))}
<ContextMenu
isOpen={isOpen}
position={position}
onClose={closeMenu}
options={getMenuOptions(targetElement)}
focusedIndex={focusedIndex}
setFocusedIndex={setFocusedIndex}
targetElement={targetElement}
/>
</>
);
}Complex Menu Structure
const complexMenuOptions = [
{
type: 'header' as const,
label: 'ACTIONS'
},
{
label: 'New File',
onClick: () => console.log('New file'),
icon: '📄',
shortcut: 'Ctrl+N'
},
{
label: 'New Folder',
onClick: () => console.log('New folder'),
icon: '📁',
shortcut: 'Ctrl+Shift+N'
},
{ type: 'separator' as const },
{
type: 'header' as const,
label: 'EDIT'
},
{
label: 'Cut',
onClick: () => console.log('Cut'),
icon: '✂️',
shortcut: 'Ctrl+X'
},
{
label: 'Copy',
onClick: () => console.log('Copy'),
icon: '📋',
shortcut: 'Ctrl+C'
},
{
label: 'Paste',
onClick: () => console.log('Paste'),
icon: '📄',
shortcut: 'Ctrl+V',
disabled: true // Example of disabled state
},
{ type: 'separator' as const },
{
label: 'Delete',
onClick: () => console.log('Delete'),
icon: '🗑️',
danger: true
}
];Custom Styling
<ContextMenu
className="custom-context-menu"
maxHeight={300}
minWidth={220}
closeOnOptionClick={false} // Keep menu open after option click
{...props}
/>🎹 Keyboard Navigation
The context menu supports full keyboard navigation:
| Key | Action |
|-----|--------|
| ↑ ↓ | Navigate between options |
| Enter Space | Select focused option |
| Escape | Close menu |
| Home | Focus first option |
| End | Focus last option |
📚 API Reference
useContextMenu() Hook
Returns an object with the following properties:
| Property | Type | Description |
|----------|------|-------------|
| isOpen | boolean | Whether the context menu is currently open |
| position | {x: number, y: number} | Current position of the menu |
| targetElement | any | Element that was right-clicked (if provided) |
| focusedIndex | number | Currently focused menu option index |
| setFocusedIndex | (index: number) => void | Function to update focused index |
| handleContextMenu | (event, element?) => void | Handler for context menu events |
| closeMenu | () => void | Function to close the menu |
ContextMenu Component Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| isOpen | boolean | - | Required. Whether menu is visible |
| position | {x: number, y: number} | - | Required. Menu position |
| onClose | () => void | - | Required. Close handler |
| options | ContextMenuOption[] | [] | Array of menu options |
| targetElement | any | null | Element that triggered the menu |
| className | string | '' | Additional CSS classes |
| focusedIndex | number | -1 | Currently focused option index |
| setFocusedIndex | (index: number) => void | - | Focus index setter |
| theme | 'light' \| 'dark' \| ContextMenuTheme | 'light' | Theme configuration |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Menu size variant |
| maxHeight | number | 400 | Maximum menu height in pixels |
| minWidth | number | - | Minimum menu width (overrides size) |
| closeOnOptionClick | boolean | true | Whether to close menu after option click |
ContextMenuOption Interface
interface ContextMenuOption {
key?: string; // Unique key for React rendering
label: string; // Display text
icon?: React.ReactNode; // Icon component or emoji
onClick?: (targetElement?: any) => void; // Click handler
disabled?: boolean; // Whether option is disabled
danger?: boolean; // Whether to style as dangerous action
shortcut?: string; // Keyboard shortcut text to display
type?: 'separator' | 'header' | 'option'; // Option type
}ContextMenuTheme Interface
interface ContextMenuTheme {
background: string; // Menu background color
border: string; // Border color
text: string; // Primary text color
textMuted: string; // Muted text color (shortcuts, icons)
hover: string; // Hover background color
hoverDanger: string; // Hover background for danger items
danger: string; // Danger text color
separator: string; // Separator line color
disabled: string; // Disabled text color
shadow: string; // Box shadow CSS
focused: string; // Keyboard focus background
focusedDanger: string; // Keyboard focus background for danger items
}🎯 Examples
Text Editor Context Menu
function TextEditor() {
const { isOpen, position, handleContextMenu, closeMenu, focusedIndex, setFocusedIndex } = useContextMenu();
const textOptions = [
{ label: 'Undo', onClick: () => {}, shortcut: 'Ctrl+Z', icon: '↶' },
{ label: 'Redo', onClick: () => {}, shortcut: 'Ctrl+Y', icon: '↷' },
{ type: 'separator' as const },
{ label: 'Cut', onClick: () => {}, shortcut: 'Ctrl+X', icon: '✂️' },
{ label: 'Copy', onClick: () => {}, shortcut: 'Ctrl+C', icon: '📋' },
{ label: 'Paste', onClick: () => {}, shortcut: 'Ctrl+V', icon: '📄' },
{ type: 'separator' as const },
{ label: 'Select All', onClick: () => {}, shortcut: 'Ctrl+A', icon: '🔘' },
];
return (
<>
<textarea
onContextMenu={handleContextMenu}
className="w-full h-40 p-4 border rounded-lg"
placeholder="Right-click for editing options..."
/>
<ContextMenu
isOpen={isOpen}
position={position}
onClose={closeMenu}
options={textOptions}
focusedIndex={focusedIndex}
setFocusedIndex={setFocusedIndex}
theme="light"
size="md"
/>
</>
);
}Image Gallery Context Menu
function ImageGallery() {
const { isOpen, position, targetElement, handleContextMenu, closeMenu, focusedIndex, setFocusedIndex } = useContextMenu();
const images = [
{ id: 1, src: 'image1.jpg', name: 'Sunset' },
{ id: 2, src: 'image2.jpg', name: 'Mountain' },
{ id: 3, src: 'image3.jpg', name: 'Ocean' },
];
const getImageOptions = (image) => [
{ type: 'header' as const, label: image.name.toUpperCase() },
{ label: 'View Full Size', onClick: () => console.log('View', image), icon: '🔍' },
{ label: 'Download', onClick: () => console.log('Download', image), icon: '💾' },
{ label: 'Share', onClick: () => console.log('Share', image), icon: '📤' },
{ type: 'separator' as const },
{ label: 'Set as Wallpaper', onClick: () => console.log('Set wallpaper', image), icon: '🖼️' },
{ type: 'separator' as const },
{ label: 'Delete', onClick: () => console.log('Delete', image), danger: true, icon: '🗑️' }
];
return (
<>
<div className="grid grid-cols-3 gap-4">
{images.map(image => (
<img
key={image.id}
src={image.src}
alt={image.name}
onContextMenu={(e) => handleContextMenu(e, image)}
className="w-full h-40 object-cover rounded-lg cursor-pointer hover:opacity-80"
/>
))}
</div>
<ContextMenu
isOpen={isOpen}
position={position}
onClose={closeMenu}
options={targetElement ? getImageOptions(targetElement) : []}
focusedIndex={focusedIndex}
setFocusedIndex={setFocusedIndex}
theme="dark"
size="lg"
/>
</>
);
}🔄 Migration from v0.x
If you're upgrading from an earlier version, here are the main changes:
Breaking Changes
themeprop now accepts string values'light'or'dark'in addition to theme objectssizeprop added with predefined size variants- Some theme property names have been updated for consistency
New Features
- Built-in light and dark themes
- Size variants (sm, md, lg)
- Enhanced keyboard navigation (Home/End keys)
- Better TypeScript support
🤝 Contributing
We welcome contributions! Please contact us for details.
Development Setup
git clone https://github.com/yourusername/react-smart-contextmenu.git
cd react-smart-contextmenu
npm install
npm run devRunning Tests
npm test
npm run test:watchBuilding
npm run build
npm run type-check📄 License
MIT © Tiredboy
🙏 Acknowledgments
- Framer Motion for smooth animations
- React team for the amazing framework
- All contributors who help make this library better
Made with ❤️ by developers, for developers
