Skip to content

Context Menu

ContextMenuManager provides a right-click context menu rendered as a DOM overlay. The menu supports keyboard navigation (arrow keys to move, Enter to select, Escape to close) and adapts its items based on click context — cell, header, row-number, or corner.

Live Demo
Right-click cells, headers, row numbers, or the corner to see context-specific menus with custom items.
Right-click any cell, header, or row number to see the context menu.
View source code
ContextMenuDemo.tsx
import { useRef, useEffect, useState } from 'react';
import { Spreadsheet } from '@witqq/spreadsheet-react';
import type { SpreadsheetRef } from '@witqq/spreadsheet-react';
import { DemoWrapper } from './DemoWrapper';
import { generateEmployees, employeeColumns } from './generate-data';
import { useSiteTheme } from './useSiteTheme';
const data = generateEmployees(50);
export function ContextMenuDemo() {
const { witTheme } = useSiteTheme();
const tableRef = useRef<SpreadsheetRef>(null);
const [lastAction, setLastAction] = useState(
'Right-click any cell, header, or row number to see the context menu.',
);
useEffect(() => {
const engine = tableRef.current?.getInstance();
if (!engine) return;
const cm = engine.getContextMenuManager();
if (!cm) return;
cm.registerItem({
id: 'highlight-row',
label: '🟡 Highlight Row',
contexts: ['cell', 'row-number'],
action: (ctx) => setLastAction(`Highlighted row ${(ctx.row ?? 0) + 1}`),
});
cm.registerItem({
id: 'column-info',
label: 'ℹ️ Column Info',
contexts: ['header'],
action: (ctx) => {
const col = employeeColumns[ctx.col ?? 0];
setLastAction(`Column: ${col?.title ?? 'unknown'}, type: ${col?.type ?? 'string'}`);
},
});
cm.registerItem({
id: 'select-all',
label: '☐ Select All',
shortcut: 'Ctrl+A',
contexts: ['corner'],
action: () => setLastAction('Select all triggered from corner'),
});
return () => {
cm.unregisterItem('highlight-row');
cm.unregisterItem('column-info');
cm.unregisterItem('select-all');
};
}, []);
return (
<DemoWrapper
title="Live Demo"
description="Right-click cells, headers, row numbers, or the corner to see context-specific menus with custom items."
height={440}
>
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div
style={{
padding: '0.5rem 0.75rem',
fontSize: '0.8rem',
color: '#64748b',
borderBottom: '1px solid #e2e8f0',
flexShrink: 0,
}}
>
{lastAction}
</div>
<div style={{ flex: 1 }}>
<Spreadsheet
theme={witTheme}
ref={tableRef}
columns={employeeColumns}
data={data}
showRowNumbers
style={{ width: '100%', height: '100%' }}
/>
</div>
</div>
</DemoWrapper>
);
}

Add custom items to the context menu through the engine API:

import { useRef, useEffect } from 'react';
import { Spreadsheet, SpreadsheetRef } from '@witqq/spreadsheet-react';
function App() {
const ref = useRef<SpreadsheetRef>(null);
useEffect(() => {
const cm = ref.current?.getInstance().getContextMenuManager();
cm?.registerItem({
id: 'insert-row-above',
label: 'Insert Row Above',
shortcut: 'Ctrl+Shift+I',
contexts: ['cell', 'row-number'],
action: (ctx) => {
console.log('Insert row above', ctx.row);
},
});
cm?.registerItem({
id: 'format-column',
label: 'Format Column',
icon: '🎨',
contexts: ['header'],
action: (ctx) => {
console.log('Format column', ctx.col);
},
isDisabled: (ctx) => ctx.col === 0,
});
return () => {
cm?.unregisterItem('insert-row-above');
cm?.unregisterItem('format-column');
};
}, []);
return <Spreadsheet ref={ref} columns={columns} data={data} />;
}

Items appear based on their contexts array:

ContextTrigger AreaTypical Actions
cellAny data cellCopy, paste, insert, delete
headerColumn headerSort, filter, resize, format
row-numberRow number gutterInsert row, delete row
cornerTop-left corner cellSelect all, clear all
MethodSignatureDescription
registerItem(item: ContextMenuItem) => voidAdd menu item
unregisterItem(id: string) => voidRemove menu item
getItems() => ReadonlyMap<string, ContextMenuItem>Get all registered items
close() => voidProgrammatically close menu
isOpenboolean (getter)Whether the menu is currently open
interface ContextMenuItem {
readonly id: string;
readonly label: string;
readonly icon?: string;
readonly shortcut?: string;
readonly separator?: boolean;
readonly contexts: ReadonlyArray<'cell' | 'header' | 'row-number' | 'corner'>;
readonly submenu?: ReadonlyArray<ContextMenuItem>;
action?: (ctx: MenuActionContext) => void;
isDisabled?: (ctx: MenuActionContext) => boolean;
isVisible?: (ctx: MenuActionContext) => boolean;
}

The callback context passed to action, isDisabled, and isVisible:

interface MenuActionContext {
readonly row: number;
readonly col: number;
readonly region: string;
readonly engine: SpreadsheetEngine;
}

Items with a submenu property render a nested menu panel. Submenus are recursive — each child can have its own submenu:

cm?.registerItem({
id: 'format-menu',
label: 'Format',
contexts: ['cell'],
submenu: [
{
id: 'format-bold',
label: 'Bold',
shortcut: 'Ctrl+B',
contexts: ['cell'],
action: (ctx) => { /* apply bold */ },
},
{
id: 'format-align',
label: 'Alignment',
contexts: ['cell'],
submenu: [
{ id: 'align-left', label: 'Left', contexts: ['cell'], action: () => {} },
{ id: 'align-center', label: 'Center', contexts: ['cell'], action: () => {} },
{ id: 'align-right', label: 'Right', contexts: ['cell'], action: () => {} },
],
},
],
});

Submenu items display a chevron; leaf items display their keyboard shortcut.

Submenus open to the right of the parent item. If there is not enough space on the right, they flip to the left. Vertical position is clamped to the container bounds.

  • Hover: Opens the submenu after a 200ms delay. Moving to a sibling item closes deeper submenus immediately.
  • Leave: Closes the submenu after 150ms. Cancelled if the mouse enters the submenu panel.
  • Click: On a submenu parent, opens the submenu immediately. On a leaf item, executes the action and closes the entire menu.
KeyBehavior
Arrow RightOpen submenu of focused item, move focus to first child
Arrow LeftClose current submenu, return focus to parent
Arrow Down/UpMove focus within current level (wraps around, skips disabled items)
EscapeClose current submenu; if at root level, close entire menu
EnterExecute focused item action (or open submenu)

Empty submenus (all children hidden via isVisible returning false) are automatically hidden from the menu.

When using the context menu as a plugin, use createContextMenuPlugin and its helper functions:

import { createContextMenuPlugin } from '@witqq/spreadsheet-plugins';
import { createDefaultMenuItems } from '@witqq/spreadsheet';
const plugin = createContextMenuPlugin();
engine.installPlugin(plugin);
// Get the default items (copy, paste, insert row, delete row, etc.)
const defaultItems = createDefaultMenuItems();

Register or unregister items via the plugin API:

import { registerMenuItem, unregisterMenuItem } from '@witqq/spreadsheet-plugins';
registerMenuItem(pluginApi, {
id: 'custom-action',
label: 'Custom Action',
contexts: ['cell'],
action: (ctx) => console.log('Custom action on', ctx.row, ctx.col),
});
unregisterMenuItem(pluginApi, 'custom-action');