Skip to content

felipecarrillo100/replace-react-contexify

Repository files navigation

replace-react-contexify

Modern React Context Menu Library - Zero Dependencies - React 16.8 to 19

npm version license GitHub

🔗 Live Demo | 📦 NPM Package | 📂 GitHub Repository

Features

  • 🚀 Modern React - Functional components with hooks
  • 📦 Zero Dependencies - Only React as peer dependency
  • 🎨 Themeable - Built-in light & dark themes, or create your own
  • Animated - Fade, flip, pop, zoom animations
  • 📱 Touch Support - Works with mouse and touch events
  • 🔧 Flexible - Declarative or programmatic API
  • 📝 JSON-driven - Build menus from JSON configuration
  • 🌍 i18n Ready - Optional message formatter for internationalization
  • 🔄 RTL Support - Full Right-to-Left language support
  • 💪 TypeScript - Full type definitions included

Installation

npm install replace-react-contexify
# or
yarn add replace-react-contexify
# or
pnpm add replace-react-contexify

Note: Don't forget to import the styles:

import 'replace-react-contexify/styles.css';

Quick Start

Basic Usage (Declarative)

import { Menu, Item, Separator, Submenu, MenuProvider } from 'replace-react-contexify';
import 'replace-react-contexify/styles.css';

const handleClick = ({ event, props }) => console.log(event, props);

// Define your menu
const MyMenu = () => (
  <Menu id="menu-id" theme="dark" animation="pop">
    <Item onClick={handleClick}>Copy</Item>
    <Item onClick={handleClick}>Cut</Item>
    <Separator />
    <Item disabled>Paste (disabled)</Item>
    <Submenu label="More Options">
      <Item onClick={handleClick}>Option A</Item>
      <Item onClick={handleClick}>Option B</Item>
    </Submenu>
  </Menu>
);

// Wrap your trigger element
const App = () => (
  <div>
    <MenuProvider id="menu-id">
      <div>Right-click me!</div>
    </MenuProvider>
    <MyMenu />
  </div>
);

Programmatic API

import { Menu, Item, contextMenu } from 'replace-react-contexify';
import 'replace-react-contexify/styles.css';

const App = () => {
  const handleContextMenu = (e: React.MouseEvent) => {
    e.preventDefault();
    contextMenu.show({
      id: 'my-menu',
      event: e,
      props: { data: 'custom data' }
    });
  };

  // Or show at specific coordinates
  const showAtPosition = () => {
    contextMenu.show({
      id: 'my-menu',
      x: 200,
      y: 300,
      props: { data: 'coordinate trigger' }
    });
  };

  return (
    <div>
      <button onContextMenu={handleContextMenu}>Right-click me</button>
      <button onClick={showAtPosition}>Show menu at (200, 300)</button>
      <Menu id="my-menu" theme="light">
        <Item onClick={({ props }) => console.log(props)}>Action</Item>
      </Menu>
    </div>
  );
};

JSON-driven Menu

import { JsonContextMenu, type JsonContextMenuRef, type ContextMenuContent } from 'replace-react-contexify';
import 'replace-react-contexify/styles.css';

const App = () => {
  const menuRef = useRef<JsonContextMenuRef>(null);

  const menuContent: ContextMenuContent = {
    items: [
      { label: 'Edit', action: () => console.log('Edit') },
      { label: 'Copy', icon: <span>📋</span>, action: () => console.log('Copy') },
      { separator: true },
      { 
        label: 'Enable Feature',
        checkbox: { enabled: true, value: false },
        action: () => console.log('Toggle')
      },
      {
        label: 'More',
        items: [
          { label: 'Sub Item 1', action: () => console.log('Sub 1') },
          { label: 'Sub Item 2', action: () => console.log('Sub 2') },
        ]
      }
    ]
  };

  const showMenu = (e: React.MouseEvent) => {
    e.preventDefault();
    menuRef.current?.show({
      event: e,
      contextMenu: menuContent
    });
  };

  return (
    <div>
      <button onContextMenu={showMenu}>Right-click for JSON Menu</button>
      <JsonContextMenu ref={menuRef} id="json-menu" theme="dark" />
    </div>
  );
};

i18n Support

import { JsonContextMenu, type MessageFormatter, type ContextMenuPredefinedMessage } from 'replace-react-contexify';

// Your i18n formatter (e.g., from react-intl)
const formatMessage: MessageFormatter = (msg: ContextMenuPredefinedMessage) => {
  const translations: Record<string, string> = {
    'menu.edit': 'Edit',
    'menu.copy': 'Copy',
    'menu.deleteCount': 'Delete {count} items',
    'menu.createdBy': 'Created by {author}',
  };
  
  let text = translations[msg.id] || msg.defaultMessage || msg.id;
  
  // Replace {key} placeholders with values
  if (msg.values) {
    Object.entries(msg.values).forEach(([key, value]) => {
      text = text.replace(`{${key}}`, String(value));
    });
  }
  
  return text;
};

const menuContent = {
  items: [
    { label: { id: 'menu.edit', defaultMessage: 'Edit' }, action: () => {} },
    { label: { id: 'menu.copy', defaultMessage: 'Copy' }, action: () => {} },
    { separator: true },
    // Using values for interpolation
    { 
      label: { 
        id: 'menu.deleteCount', 
        defaultMessage: 'Delete {count} items',
        values: { count: 5 }  // Will render: "Delete 5 items"
      },
      action: () => {}
    },
    { 
      label: { 
        id: 'menu.createdBy', 
        defaultMessage: 'Created by {author}',
        values: { author: 'John' }  // Will render: "Created by John"
      },
      action: () => {}
    },
  ]
};

<JsonContextMenu 
  ref={menuRef} 
  id="i18n-menu" 
  formatMessageProvider={formatMessage}
/>

With react-intl:

import { useIntl } from 'react-intl';
import { JsonContextMenu, type MessageFormatter } from 'replace-react-contexify';

const App = () => {
  const intl = useIntl();
  
  // react-intl's formatMessage works directly!
  const formatMessage: MessageFormatter = (msg) => {
    return intl.formatMessage(
      { id: msg.id, defaultMessage: msg.defaultMessage },
      msg.values
    );
  };

  return <JsonContextMenu ref={menuRef} id="menu" formatMessageProvider={formatMessage} />;
};

RTL (Right-to-Left) Support

Full support for RTL languages like Arabic, Hebrew, Persian, etc.

// Just wrap with dir="rtl" - everything works automatically!
<div dir="rtl">
  <MenuProvider id="rtl-menu">
    <div>انقر بالزر الأيمن هنا</div>
  </MenuProvider>

  <Menu id="rtl-menu" theme="dark">
    <Item onClick={handleClick}>📋 نسخ (Copy)</Item>
    <Item onClick={handleClick}>✂️ قص (Cut)</Item>
    <Submenu label="📁 المزيد (More)">
      <Item onClick={handleClick}>الخيار أ</Item>
      <Item onClick={handleClick}>الخيار ب</Item>
    </Submenu>
  </Menu>
</div>

RTL support features:

  • ✅ Text alignment automatically flips to right
  • ✅ Submenu arrows point left instead of right
  • ✅ Submenus open to the left
  • ✅ Icon positions flip appropriately
  • ✅ Detected from dir="rtl" on any parent element or html[dir="rtl"]

Styling

Using Compiled CSS (Recommended)

Import the pre-compiled CSS file:

import 'replace-react-contexify/styles.css';

Using SCSS (for customization)

For full control over styling, import the SCSS source files:

// Import all styles
@use 'replace-react-contexify/scss/main';

// Or import individual modules
@use 'replace-react-contexify/scss/menu';
@use 'replace-react-contexify/scss/rtl';
@use 'replace-react-contexify/scss/json-menu';
@use 'replace-react-contexify/scss/themes/dark';
@use 'replace-react-contexify/scss/themes/light';
@use 'replace-react-contexify/scss/animations/fade';
@use 'replace-react-contexify/scss/animations/pop';

Available SCSS Files

replace-react-contexify/scss/
├── main.scss           # All styles bundled
├── _menu.scss          # Core menu styles
├── _rtl.scss           # RTL support
├── _json-menu.scss     # JSON menu specifics
├── animations/
│   ├── _fade.scss
│   ├── _flip.scss
│   ├── _pop.scss
│   └── _zoom.scss
└── themes/
    ├── _dark.scss
    └── _light.scss

Themes

Built-in themes: light, dark

<Menu id="my-menu" theme="dark">
  ...
</Menu>

Animations

Built-in animations: fade, flip, pop, zoom

<Menu id="my-menu" animation="pop">
  ...
</Menu>

Components

Component Description
Menu Container for menu items, positioned at trigger location
Item Individual menu item with click handler
Separator Visual divider between menu items
Submenu Nested menu with child items
MenuProvider Wrapper that binds context menu events to children
JsonContextMenu Menu rendered from JSON configuration
IconFont Helper for displaying icons

API Reference

Menu

The main container component for context menu items.

<Menu
  id="my-menu"              // Required: unique identifier
  theme="dark"              // Optional: 'light' | 'dark' | custom string
  animation="pop"           // Optional: 'fade' | 'flip' | 'pop' | 'zoom'
  className="custom-class"  // Optional: additional CSS classes
  style={{ minWidth: 200 }} // Optional: inline styles
>
  {children}
</Menu>
Prop Type Required Description
id string | number Unique identifier to trigger this menu
theme string Built-in: 'light', 'dark' or custom
animation string Built-in: 'fade', 'flip', 'pop', 'zoom'
className string Additional CSS classes
style CSSProperties Inline styles

Item

Individual clickable menu item.

<Item
  onClick={handler}         // Click handler
  disabled={false}          // Static disabled state
  disabled={(args) => bool} // Dynamic disabled state
  data={{ key: 'value' }}   // Custom data passed to onClick
  className="custom"        // Additional CSS classes
  style={{}}                // Inline styles
>
  📋 Copy
</Item>
Prop Type Required Description
onClick (args: MenuItemEventHandler) => void Click handler
disabled boolean | ((args: MenuItemEventHandler) => boolean) Disable the item
data Record<string, unknown> Custom data passed to handlers via props
children ReactNode Item content
className string Additional CSS classes
style CSSProperties Inline styles

MenuItemEventHandler:

interface MenuItemEventHandler {
  event: TriggerEvent;                // The original trigger event
  props?: Record<string, unknown>;    // Combined data from trigger + item
}

Submenu

Nested menu with child items. Supports infinite nesting.

<Submenu
  label="📁 More Options"   // Required: submenu label
  arrow="▶"                 // Optional: custom arrow character
  disabled={false}          // Optional: disable submenu
>
  <Item onClick={handler}>Sub Item 1</Item>
  <Item onClick={handler}>Sub Item 2</Item>
</Submenu>
Prop Type Required Description
label ReactNode Submenu label content
arrow ReactNode Custom arrow (default: , RTL: )
disabled boolean | ((args) => boolean) Disable the submenu
children ReactNode Submenu items
className string Additional CSS classes
style CSSProperties Inline styles

Separator

Visual divider between menu items.

<Separator />

MenuProvider

Wrapper that binds context menu events to children. Right-click triggers the menu.

<MenuProvider
  id="my-menu"              // Required: menu id to trigger
  data={{ context: 'data' }} // Optional: data passed to menu items
  className="trigger-area"  // Optional: CSS classes
>
  <div>Right-click me!</div>
</MenuProvider>
Prop Type Required Description
id string | number Menu ID to trigger
data Record<string, unknown> Data passed to item handlers via props
children ReactNode Trigger element(s)
className string Additional CSS classes
style CSSProperties Inline styles

contextMenu

Programmatic API for showing/hiding menus.

import { contextMenu } from 'replace-react-contexify';

// Show menu at event position
contextMenu.show({
  id: 'my-menu',
  event: mouseEvent,
  props: { custom: 'data' }
});

// Show menu at specific coordinates
contextMenu.show({
  id: 'my-menu',
  x: 300,
  y: 200,
  props: { custom: 'data' }
});

// Hide all menus
contextMenu.hideAll();
Method Parameters Description
show(options) ShowContextMenuParams Display a menu
hideAll() none Hide all visible menus

ShowContextMenuParams:

interface ShowContextMenuParams {
  id: MenuId;                          // Menu ID to show
  event?: MouseEvent | TouchEvent;     // Position from event
  x?: number;                          // X coordinate (if no event)
  y?: number;                          // Y coordinate (if no event)
  props?: Record<string, unknown>;     // Data passed to items
}

JsonContextMenu

Dynamic menu rendered from JSON configuration. Perfect for API-driven menus.

const menuRef = useRef<JsonContextMenuRef>(null);

<JsonContextMenu
  ref={menuRef}                        // Required: ref for imperative control
  id="json-menu"                       // Required: unique identifier
  theme="dark"                         // Optional: theme
  animation="pop"                      // Optional: animation
  formatMessageProvider={formatMessage} // Optional: i18n formatter
/>

// Show the menu
menuRef.current?.show({
  event: mouseEvent,                   // or x/y coordinates
  contextMenu: menuContent             // ContextMenuContent object
});
Prop Type Required Description
ref Ref<JsonContextMenuRef> Ref for imperative show() method
id string | number Unique identifier
theme string Theme name
animation string Animation name
formatMessageProvider MessageFormatter i18n formatter function

JsonContextMenuRef:

interface JsonContextMenuRef {
  show: (options: ShowJsonContextMenuOptions) => void;
}

interface ShowJsonContextMenuOptions {
  x?: number;                          // X coordinate
  y?: number;                          // Y coordinate
  event?: MouseEvent | TouchEvent;     // Or position from event
  contextMenu: ContextMenuContent;     // Menu content
}

ContextMenuContent

JSON structure for defining menu content.

interface ContextMenuContent {
  items: ContextMenuItem[];
}

// Item types:
type ContextMenuItem = 
  | ContextMenuSimpleItem    // Regular item
  | ContextMenuSeparator     // Separator
  | ContextMenuSubMenu;      // Submenu with nested items

Simple Item:

interface ContextMenuSimpleItem {
  label: string | ContextMenuPredefinedMessage;  // Display text or i18n
  icon?: ReactNode;                              // Optional icon
  title?: string | ContextMenuPredefinedMessage; // Tooltip
  checkbox?: {
    enabled: boolean;                            // Clickable?
    value: boolean;                              // Checked state
  };
  action?: (event: TriggerEvent) => void;        // Click handler
}

Separator:

interface ContextMenuSeparator {
  separator: true;
}

Submenu:

interface ContextMenuSubMenu {
  label: string | ContextMenuPredefinedMessage;
  title?: string | ContextMenuPredefinedMessage;
  items: ContextMenuItem[];                      // Nested items
}

i18n / MessageFormatter

For internationalization support, provide a formatMessageProvider function.

type MessageFormatter = (message: ContextMenuPredefinedMessage) => string;

interface ContextMenuPredefinedMessage {
  id: string;                                    // Message ID for lookup
  defaultMessage?: string;                       // Fallback text
  values?: Record<string, string | number>;      // Interpolation values
}

With react-intl:

import { useIntl } from 'react-intl';

const intl = useIntl();

const formatMessage: MessageFormatter = (msg) => {
  return intl.formatMessage(
    { id: msg.id, defaultMessage: msg.defaultMessage },
    msg.values  // Supports {count}, {name}, etc.
  );
};

<JsonContextMenu
  ref={menuRef}
  id="i18n-menu"
  formatMessageProvider={formatMessage}
/>

Type Guards

Utility functions for working with menu items:

import { 
  isSeparator, 
  isSubMenu, 
  isSimpleItem, 
  isPredefinedMessage,
  resolveLabel 
} from 'replace-react-contexify';

// Check item types
isSeparator(item)        // item is ContextMenuSeparator
isSubMenu(item)          // item is ContextMenuSubMenu  
isSimpleItem(item)       // item is ContextMenuSimpleItem
isPredefinedMessage(label) // label is ContextMenuPredefinedMessage

// Resolve label to string
resolveLabel(label, formatMessage?)  // Returns string

Types

All TypeScript types are exported:

import type {
  // Component Props
  MenuProps,
  ItemProps,
  SubmenuProps,
  SeparatorProps,
  MenuProviderProps,
  JsonContextMenuProps,
  
  // Event & Handler Types
  TriggerEvent,
  MenuId,
  MenuItemEventHandler,
  
  // JSON Menu Types
  ContextMenuContent,
  ContextMenuItem,
  ContextMenuSimpleItem,
  ContextMenuSubMenu,
  ContextMenuSeparator,
  ContextMenuCheckbox,
  ContextMenuLabel,
  
  // i18n Types
  ContextMenuPredefinedMessage,
  MessageFormatter,
  
  // Ref Types
  JsonContextMenuRef,
  ShowJsonContextMenuOptions,
} from 'replace-react-contexify';

Browser Support

  • Chrome (latest)
  • Firefox (latest)
  • Safari (latest)
  • Edge (latest)

React Compatibility

  • React 16.8+ (hooks support required)
  • React 17
  • React 18
  • React 19

Development

# Install dependencies
npm install

# Run demo
npm run dev

# Run tests
npm test

# Build library
npm run build

Acknowledgments

This project was inspired by and built upon the foundation of react-contexify by Fadi Khadra. Thank you for creating such a great library that served the React community for years! 🙏

License

MIT

About

Modern React context menu library with zero dependencies. Features themes, animations, JSON-driven menus, i18n support with message interpolation, RTL languages, and full TypeScript support. Works with React 16.8 to 19.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors