Context menu
The context menu provides cell-level actions accessible by right-clicking. This page lists all available menu items and their configuration keys.
Context menu with default options
Enable the context menu with the default configuration:
contextMenu: true,To see the context menu, right-click on a cell. On touch devices, long-press a cell to open the context menu.
import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';
// Register all Handsontable's modules.registerAllModules();
const container = document.querySelector('#example1');
new Handsontable(container, { data: [ ['', 'Tesla', 'Nissan', 'Toyota', 'Honda', 'Mazda', 'Ford'], ['2017', 10, 11, 12, 13, 15, 16], ['2018', 10, 11, 12, 13, 15, 16], ['2019', 10, 11, 12, 13, 15, 16], ['2020', 10, 11, 12, 13, 15, 16], ['2021', 10, 11, 12, 13, 15, 16], ], rowHeaders: true, colHeaders: true, contextMenu: true, height: 'auto', autoWrapRow: true, autoWrapCol: true, licenseKey: 'non-commercial-and-evaluation',});import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';
// Register all Handsontable's modules.registerAllModules();
const container = document.querySelector('#example1')!;
new Handsontable(container, { data: [ ['', 'Tesla', 'Nissan', 'Toyota', 'Honda', 'Mazda', 'Ford'], ['2017', 10, 11, 12, 13, 15, 16], ['2018', 10, 11, 12, 13, 15, 16], ['2019', 10, 11, 12, 13, 15, 16], ['2020', 10, 11, 12, 13, 15, 16], ['2021', 10, 11, 12, 13, 15, 16], ], rowHeaders: true, colHeaders: true, contextMenu: true, height: 'auto', autoWrapRow: true, autoWrapCol: true, licenseKey: 'non-commercial-and-evaluation',});Context menu with selected options
You can define the items in the menu by passing the contextMenu option as an array of keys:
| Key | Action and required plugins |
|---|---|
row_above | Insert a row above |
row_below | Insert a row below |
col_left | Insert a column to the left |
col_right | Insert a column to the right |
--------- | Add a separator to the items in the menu |
remove_row | Remove the selected row |
remove_col | Remove the selected column |
clear_column | Delete the data of the selected columns |
undo | Undo the last action. Requires: UndoRedo |
redo | Redo the last action. Requires: UndoRedo |
make_read_only | Make the selected cells read-only |
alignment | Align the text in the cell |
cut | Cut the contents of the selected cells to the system clipboard. Requires: CopyPaste |
copy | Copy the contents of the selected cells to the system clipboard. Requires: CopyPaste |
copy_with_column_headers | Copy the contents of the selected cells and their nearest column headers. Requires: CopyPaste with copyColumnHeaders set to true |
copy_with_column_group_headers | Copy the contents of the selected cells and all their related column headers. Requires: NestedHeaders and CopyPaste with copyColumnGroupHeaders set to true |
copy_column_headers_only | Copy the contents of column headers that are nearest to the selected cells. Requires: CopyPaste with copyColumnHeadersOnly set to true |
freeze_column | Freeze the selected column. Requires: ManualColumnFreeze |
unfreeze_column | Unfreeze the selected column. Requires: ManualColumnFreeze |
borders | Add borders around the selected cells. Requires: CustomBorders |
commentsAddEdit | Add or edit a comment. Requires: Comments |
commentsRemove | Remove the comment. Requires: Comments |
commentsReadOnly | Make the comment read-only. Requires: Comments |
mergeCells | Merge or unmerge the selected cells. Requires: MergeCells |
add_child | Insert a child row. Requires: NestedRows |
detach_from_parent | Detach the selected row from its parent row. Requires: NestedRows |
hidden_columns_hide | Hide the selected columns. Requires: HiddenColumns |
hidden_columns_show | Show the hidden columns. Requires: HiddenColumns |
hidden_rows_hide | Hide the selected rows. Requires: HiddenRows |
hidden_rows_show | Show hidden rows. Requires: HiddenRows |
filter_by_condition | Add the first filter condition. Requires: Filters |
filter_by_condition2 | Add the second filter condition. Requires: Filters |
filter_operators | Select a filter parameter. Requires: Filters |
filter_by_value | Add a filter value. Requires: Filters |
filter_action_bar | Apply the configured filter. Requires: Filters |
export_file | Open the Export submenu with “To CSV” and “To Excel” items. Requires: ExportFile. The Excel item is hidden when no XLSX engine is configured. |
The filter_by_condition, filter_by_condition2, filter_operators, filter_by_value, and
filter_action_bar items build the filtering interface and take effect only in the dropdown (column)
menu, not in the context menu. See Filter menu items.
The table above lists every key you can use in the array form of contextMenu. An array key that
matches no available item is skipped, and Handsontable logs a console warning naming it and the
menu it came from. That covers a key with a typo in it, and a key whose plugin is not enabled —
so check the required plugin named beside the key when an item does not appear.
Two cases are skipped without a warning. A key for a built-in item that is turned off, such as
row_above under allowInsertRow: false, is left out on purpose. And in the object form of
contextMenu, a plain string value is the item’s label rather than a key to look up, so nothing
is resolved and nothing is reported.
To see the context menu, right-click on a cell. On touch devices, long-press a cell to open the context menu.
import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';
// Register all Handsontable's modules.registerAllModules();
const container = document.querySelector('#example2');
new Handsontable(container, { data: [ ['', 'Tesla', 'Nissan', 'Toyota', 'Honda', 'Mazda', 'Ford'], ['2017', 10, 11, 12, 13, 15, 16], ['2018', 10, 11, 12, 13, 15, 16], ['2019', 10, 11, 12, 13, 15, 16], ['2020', 10, 11, 12, 13, 15, 16], ['2021', 10, 11, 12, 13, 15, 16], ], rowHeaders: true, colHeaders: true, contextMenu: ['row_above', 'row_below', 'remove_row', 'clear_column'], height: 'auto', autoWrapRow: true, autoWrapCol: true, licenseKey: 'non-commercial-and-evaluation',});import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';
// Register all Handsontable's modules.registerAllModules();
const container = document.querySelector('#example2')!;
new Handsontable(container, { data: [ ['', 'Tesla', 'Nissan', 'Toyota', 'Honda', 'Mazda', 'Ford'], ['2017', 10, 11, 12, 13, 15, 16], ['2018', 10, 11, 12, 13, 15, 16], ['2019', 10, 11, 12, 13, 15, 16], ['2020', 10, 11, 12, 13, 15, 16], ['2021', 10, 11, 12, 13, 15, 16], ], rowHeaders: true, colHeaders: true, contextMenu: ['row_above', 'row_below', 'remove_row', 'clear_column'], height: 'auto', autoWrapRow: true, autoWrapCol: true, licenseKey: 'non-commercial-and-evaluation',});Command names are not menu item keys
Some commands are addressed with a parent:child name, such as alignment:left. Those names work
with executeCommand(), which runs a command directly:
hot.getPlugin('contextMenu').executeCommand('alignment:left');They are not menu item keys. Listing alignment:left in contextMenu does not create a “Left”
item, because only the keys in the table above are resolved.
To show part of a submenu, keep the predefined submenu items and filter out the ones you don’t want. The items you keep carry their own actions, so they keep working:
const verticalKeys = ['alignment:top', 'alignment:middle', 'alignment:bottom'];
const hot = new Handsontable(container, { licenseKey: 'non-commercial-and-evaluation', contextMenu: true, afterContextMenuDefaultOptions(options) { const alignment = options.items.find(item => item.key === 'alignment');
if (!alignment) { return; }
alignment.submenu.items = alignment.submenu.items .filter(item => !verticalKeys.includes(item.key)); },});The alignment submenu then offers only the horizontal options. For the column menu, use
afterDropdownMenuDefaultOptions instead.
To build a submenu item of your own, give it a name and a callback. A custom item runs only the
callback you write — it does not inherit an action from a predefined key of the same name.
Context menu with a fully custom configuration
To fully customize the context menu, set contextMenu to an object with an items property. Each key in items identifies one menu entry. The value can be:
- A predefined item key string such as
'row_above'— includes the built-in item unchanged. ContextMenu.SEPARATOR(imported fromhandsontable/plugins/contextMenu) — inserts a horizontal separator line.- A configuration object — defines a custom item or overrides a predefined one.
This means you can freely mix built-in items with custom ones in the same menu:
import { ContextMenu } from 'handsontable/plugins/contextMenu';
contextMenu: { // Optional: a shared callback fired on every item click. callback(key, selection, clickEvent) { console.log(key, selection, clickEvent); }, items: { row_above: {}, // predefined item, unchanged sp1: ContextMenu.SEPARATOR, // separator row_below: { name: 'Click to add row below', // override a predefined item's label }, myOption: { // fully custom item name: 'My custom action', callback() { /* ... */ }, }, },},The top-level object also accepts a uiContainer property (an HTMLElement) to control which DOM element the context menu is appended to.
Menu item configuration options
Each configuration object in items can have these properties:
| Option | Description |
|---|---|
key | The unique identifier for the item. For top-level items, this is the key of the items object (for example 'row_above' or 'myOption'). For submenu items, it must follow the parent_key:child_key format (for example 'colors:red'). |
name | The label shown in the menu. Can be a string or a function returning a string. Supports HTML — see the note below. When a function, this refers to the Handsontable instance. |
disabled | Whether the item is grayed out and non-clickable. Can be a boolean or a function returning a boolean. When a function, this refers to the Handsontable instance. |
hidden | Whether the item is hidden from the menu entirely. Can be a boolean or a function returning a boolean. When a function, this refers to the Handsontable instance. |
checked | Whether the item is marked with a check mark. Can be a boolean, the string 'mixed', or a function returning either. When a function, this refers to the Handsontable instance. Use 'mixed' when the state applies to only part of the selection: the item shows a dash instead of a check mark, and assistive technology announces it as partly checked. Setting this option also makes the item a checkbox for assistive technology, so its state is announced as well as shown. An item that defines its own renderer draws its own content, so it gets the accessible state but no mark. |
callback | A function called when the item is clicked. Receives key, selection, and clickEvent as arguments. |
submenu | Defines a nested submenu. Takes an object with an items array. Each submenu item’s key must follow the parent_key:child_key format. |
renderer | A custom function for rendering the item’s HTML. Must return an HTMLElement. A renderer owns the item’s content, so Handsontable draws no checked mark inside it. |
disableSelection | When true, hovering over the item does not highlight it. |
isCommand | When false, clicking the item does not execute a command or close the menu. |
The following example shows how to:
- Add a shared callback for all options
- Dynamically disable an option
- Override the label of a predefined option
- Add a fully custom option with its own callback
- Add a custom option with a submenu
- Use a custom renderer
To see the context menu, right-click on a cell. On touch devices, long-press a cell to open the context menu.
import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';import { ContextMenu } from 'handsontable/plugins/contextMenu';
// Register all Handsontable's modules.registerAllModules();
const ALLOWED_TAGS = ['B', 'I', 'EM', 'STRONG', 'BR', 'TABLE', 'THEAD', 'TBODY', 'TR', 'TD', 'TH'];const ALLOWED_ATTRIBUTES = ['colspan', 'rowspan'];const DROPPED_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE'];
// Handsontable has no built-in sanitizer since v18.0, and `sanitizer` is grid-level:// it also filters pasted HTML, so the table tags have to survive -- otherwise pasting// a range degrades to plain text. In production, use a vetted library such as DOMPurify.// See https://handsontable.com/docs/security/const sanitizeMenuLabel = (html) => { const template = document.createElement('template');
template.innerHTML = html;
template.content.querySelectorAll('*').forEach((element) => { if (DROPPED_TAGS.includes(element.tagName)) { // Unwrapping these would promote their source text into the output element.remove(); } else if (ALLOWED_TAGS.includes(element.tagName)) { Array.from(element.attributes).forEach((attribute) => { if (!ALLOWED_ATTRIBUTES.includes(attribute.name)) { element.removeAttribute(attribute.name); } }); } else { // Unwrap a disallowed element, keeping its text content element.replaceWith(...Array.from(element.childNodes)); } });
return template.innerHTML;};
const contextMenuSettings = { callback(key, selection, clickEvent) { // Common callback for all options console.log(key, selection, clickEvent); }, items: { row_above: { disabled() { // `disabled` can be a boolean or a function // Disable option when first row was clicked return this.getSelectedLast()?.[0] === 0; // `this` === hot }, }, // Use the dedicated separator constant to insert a separator line // (the key has to be unique) sp1: ContextMenu.SEPARATOR, row_below: { name: 'Click to add row below', // Set custom text for predefined option }, about: { // Own custom option name() { // `name` can be a string or a function return '<b>Custom option</b>'; // Name can contain HTML }, hidden() { // `hidden` can be a boolean or a function // Hide the option when the first column was clicked return this.getSelectedLast()?.[1] == 0; // `this` === hot }, callback() { // Callback for specific option setTimeout(() => { alert('Hello world!'); // Fire alert after menu close (with timeout) }, 0); }, }, colors: { // Own custom option name: 'Colors...', submenu: { // Custom option with submenu of items items: [ { // Key must be in the form 'parent_key:child_key' key: 'colors:red', name: 'Red', callback() { setTimeout(() => { alert('You clicked red!'); }, 0); }, }, { key: 'colors:green', name: 'Green', callback() { setTimeout(() => { alert('You clicked green!'); }, 0); }, }, { key: 'colors:blue', name: 'Blue', callback() { setTimeout(() => { alert('You clicked blue!'); }, 0); }, }, ], }, }, credits: { // Own custom property // Custom rendered element in the context menu renderer() { const elem = document.createElement('marquee');
elem.style.cssText = 'background: lightgray; color: #222222;'; elem.textContent = 'Brought to you by...';
return elem; }, disableSelection: true, isCommand: false, // Prevent clicks from executing command and closing the menu }, },};
const container = document.querySelector('#example3');
new Handsontable(container, { data: [ ['', 'Tesla', 'Nissan', 'Toyota', 'Honda', 'Mazda', 'Ford'], ['2017', 10, 11, 12, 13, 15, 16], ['2018', 10, 11, 12, 13, 15, 16], ['2019', 10, 11, 12, 13, 15, 16], ['2020', 10, 11, 12, 13, 15, 16], ['2021', 10, 11, 12, 13, 15, 16], ], rowHeaders: true, colHeaders: true, licenseKey: 'non-commercial-and-evaluation', height: 'auto', contextMenu: contextMenuSettings, // Required for the HTML in the `about` item's label to be rendered safely sanitizer: sanitizeMenuLabel, autoWrapRow: true, autoWrapCol: true,});import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';import { ContextMenu, DetailedSettings } from 'handsontable/plugins/contextMenu';
// Register all Handsontable's modules.registerAllModules();
const ALLOWED_TAGS = ['B', 'I', 'EM', 'STRONG', 'BR', 'TABLE', 'THEAD', 'TBODY', 'TR', 'TD', 'TH'];const ALLOWED_ATTRIBUTES = ['colspan', 'rowspan'];const DROPPED_TAGS = ['SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE'];
// Handsontable has no built-in sanitizer since v18.0, and `sanitizer` is grid-level:// it also filters pasted HTML, so the table tags have to survive -- otherwise pasting// a range degrades to plain text. In production, use a vetted library such as DOMPurify.// See https://handsontable.com/docs/security/const sanitizeMenuLabel = (html: string): string => { const template = document.createElement('template');
template.innerHTML = html;
template.content.querySelectorAll('*').forEach((element) => { if (DROPPED_TAGS.includes(element.tagName)) { // Unwrapping these would promote their source text into the output element.remove(); } else if (ALLOWED_TAGS.includes(element.tagName)) { Array.from(element.attributes).forEach((attribute) => { if (!ALLOWED_ATTRIBUTES.includes(attribute.name)) { element.removeAttribute(attribute.name); } }); } else { // Unwrap a disallowed element, keeping its text content element.replaceWith(...Array.from(element.childNodes)); } });
return template.innerHTML;};
const contextMenuSettings: DetailedSettings = { callback(key, selection, clickEvent) { // Common callback for all options console.log(key, selection, clickEvent); }, items: { row_above: { disabled() { // `disabled` can be a boolean or a function // Disable option when first row was clicked return this.getSelectedLast()?.[0] === 0; // `this` === hot }, }, // Use the dedicated separator constant to insert a separator line // (the key has to be unique) sp1: ContextMenu.SEPARATOR, row_below: { name: 'Click to add row below', // Set custom text for predefined option }, about: { // Own custom option name() { // `name` can be a string or a function return '<b>Custom option</b>'; // Name can contain HTML }, hidden() { // `hidden` can be a boolean or a function // Hide the option when the first column was clicked return this.getSelectedLast()?.[1] == 0; // `this` === hot }, callback() { // Callback for specific option setTimeout(() => { alert('Hello world!'); // Fire alert after menu close (with timeout) }, 0); }, }, colors: { // Own custom option name: 'Colors...', submenu: { // Custom option with submenu of items items: [ { // Key must be in the form 'parent_key:child_key' key: 'colors:red', name: 'Red', callback() { setTimeout(() => { alert('You clicked red!'); }, 0); }, }, { key: 'colors:green', name: 'Green', callback() { setTimeout(() => { alert('You clicked green!'); }, 0); }, }, { key: 'colors:blue', name: 'Blue', callback() { setTimeout(() => { alert('You clicked blue!'); }, 0); }, }, ], }, }, credits: { // Own custom property // Custom rendered element in the context menu renderer() { const elem = document.createElement('marquee');
elem.style.cssText = 'background: lightgray; color: #222222;'; elem.textContent = 'Brought to you by...';
return elem; }, disableSelection: true, // Prevent mouseoever from highlighting the item for selection isCommand: false, // Prevent clicks from executing command and closing the menu }, },};
const container = document.querySelector('#example3')!;
new Handsontable(container, { data: [ ['', 'Tesla', 'Nissan', 'Toyota', 'Honda', 'Mazda', 'Ford'], ['2017', 10, 11, 12, 13, 15, 16], ['2018', 10, 11, 12, 13, 15, 16], ['2019', 10, 11, 12, 13, 15, 16], ['2020', 10, 11, 12, 13, 15, 16], ['2021', 10, 11, 12, 13, 15, 16], ], rowHeaders: true, colHeaders: true, licenseKey: 'non-commercial-and-evaluation', height: 'auto', contextMenu: contextMenuSettings, // Required for the HTML in the `about` item's label to be rendered safely sanitizer: sanitizeMenuLabel, autoWrapRow: true, autoWrapCol: true,});Related keyboard shortcuts and gestures
| Windows | macOS | Action | Excel | Sheets |
|---|---|---|---|---|
| Ctrl+Shift+\ or Shift+F10 | ⌘+⇧+\ or ⇧+F10 | Open the context menu | ✗ | ✓ |
| Long-press (touch devices) | Long-press (touch devices) | Open the context menu | ✗ | ✓ |
| Arrow keys | Arrow keys | Move one available menu item up, down, left, or right | ✓ | ✓ |
| Page Up | Page Up | Move to the first visible item of the context menu or submenu | ✓ | ✗ |
| Page Down | Page Down | Move to the last visible item of the context menu or submenu | ✓ | ✗ |
| Escape | Escape | Close the context menu or submenu | ✓ | ✓ |
| Enter | Enter | Run the action of the selected menu item | ✓ | ✗ |
Related articles
Related guides
Related blog articles
Configuration options
Hooks
Plugins