Skip to content

In this tutorial, you will register custom keyboard shortcuts in Handsontable using the ShortcutManager API. You will learn how to add shortcuts like Ctrl+D to duplicate rows and Ctrl+Enter to submit grid data without conflicting with existing browser or grid shortcuts.

Shortcut log:
Last shortcut triggered
Last submission
JavaScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
registerAllModules();
/* start:skip-in-preview */
const employees = [
{ name: 'Ana García', department: 'Engineering', role: 'Senior Developer', salary: 95000, startDate: '2021-03-15' },
{ name: 'James Okafor', department: 'Marketing', role: 'Marketing Manager', salary: 82000, startDate: '2020-07-01' },
{ name: 'Li Wei', department: 'Engineering', role: 'Frontend Developer', salary: 78000, startDate: '2022-01-10' },
{ name: 'Priya Nair', department: 'HR', role: 'HR Specialist', salary: 68000, startDate: '2019-11-20' },
{ name: 'Carlos Mendes', department: 'Finance', role: 'Financial Analyst', salary: 88000, startDate: '2021-09-05' },
{ name: 'Fatima Al-Hassan', department: 'Engineering', role: 'Backend Developer', salary: 92000, startDate: '2020-04-18' },
{ name: 'Noah Kim', department: 'Design', role: 'UX Designer', salary: 75000, startDate: '2023-02-14' },
{ name: 'Sara Lindqvist', department: 'Marketing', role: 'Content Strategist', salary: 71000, startDate: '2019-06-30' },
];
/* end:skip-in-preview */
const container = document.querySelector('#example1');
const hot = new Handsontable(container, {
data: employees,
colHeaders: ['Name', 'Department', 'Role', 'Salary', 'Start Date'],
columns: [
{ data: 'name', type: 'text' },
{ data: 'department', type: 'text' },
{ data: 'role', type: 'text' },
{ data: 'salary', type: 'numeric', locale: 'en-US', numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0 } },
{ data: 'startDate', type: 'text' },
],
rowHeaders: true,
height: 'auto',
width: '100%',
autoWrapRow: true,
licenseKey: 'non-commercial-and-evaluation',
});
const preview = container.closest('.hot-example-preview') ?? container.parentElement;
const lastShortcutEl = preview.querySelector('.last-shortcut');
const lastSubmissionEl = preview.querySelector('.last-submission');
const gridContext = hot.getShortcutManager().getContext('grid');
// Ctrl+D: duplicate the currently selected row
gridContext.addShortcut({
keys: [['Control', 'd']],
group: 'customActions',
runOnlyIf: () => hot.getSelected() !== undefined,
callback: (event) => {
event.preventDefault();
const selectedRange = hot.getSelectedRangeLast();
if (!selectedRange) {
return;
}
const row = selectedRange.from.row;
const rowData = hot.getSourceDataAtRow(row);
hot.alter('insert_row_below', row);
hot.populateFromArray(row + 1, 0, [Object.values(rowData)]);
lastShortcutEl.textContent = 'Ctrl+D -- row duplicated';
},
});
// Ctrl+Enter: submit the grid data
gridContext.addShortcut({
keys: [['Control', 'Enter']],
group: 'customActions',
runOnlyIf: () => true,
callback: (event) => {
event.preventDefault();
const data = hot.getData();
const headers = hot.getColHeader();
const rowCount = data.length;
const timestamp = new Date().toLocaleTimeString();
lastShortcutEl.textContent = 'Ctrl+Enter -- data submitted';
lastSubmissionEl.textContent = `[${timestamp}] Submitted ${rowCount} rows -- columns: ${headers.join(', ')}`;
},
});
HTML
<div id="example1"></div>
<strong>Shortcut log:</strong>
<table class="debug-table">
<colgroup>
<col style="width: 180px">
<col>
</colgroup>
<tbody>
<tr>
<td>Last shortcut triggered</td>
<td><code class="last-shortcut">—</code></td>
</tr>
<tr>
<td>Last submission</td>
<td><code class="last-submission">—</code></td>
</tr>
</tbody>
</table>

Overview

Difficulty: Beginner Time: ~15 minutes

Handsontable’s ShortcutManager API lets you register custom keyboard shortcuts that fire only when the grid has focus. This recipe shows how to add two common shortcuts: Ctrl+D to duplicate a selected row, and Ctrl+Enter to collect and submit the grid’s data.

What You’ll Build

A grid with two custom shortcuts:

  • Ctrl+D — duplicates the currently selected row and inserts the copy directly below it.
  • Ctrl+Enter — reads all grid data and displays a submission summary in the shortcut log.
  • A shortcut log table below the grid that shows the last shortcut triggered and the last submission.

Before you begin

This recipe requires no extra dependencies beyond Handsontable itself. You should be familiar with:

  • Creating a basic Handsontable instance.
  • Reading cell data with hot.getData() and hot.getSourceDataAtRow().
  • Modifying rows with hot.alter() and hot.populateFromArray().
  1. Set up the grid

    Import Handsontable, register all modules, and initialize the grid with your data.

    import Handsontable from 'handsontable/base';
    import { registerAllModules } from 'handsontable/registry';
    registerAllModules();
    const container = document.querySelector('#example1');
    const hot = new Handsontable(container, {
    data: employees,
    colHeaders: ['Name', 'Department', 'Role', 'Salary', 'Start Date'],
    columns: [
    { data: 'name', type: 'text' },
    { data: 'department', type: 'text' },
    { data: 'role', type: 'text' },
    { data: 'salary', type: 'numeric', locale: 'en-US', numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 0, maximumFractionDigits: 0 } },
    { data: 'startDate', type: 'text' },
    ],
    rowHeaders: true,
    height: 'auto',
    licenseKey: 'non-commercial-and-evaluation',
    });

    What’s happening:

    • registerAllModules() registers all built-in cell types, editors, renderers, and plugins — including the ShortcutManager.
    • The grid uses an array of objects as its data source. Each property maps to a column via the data key.
  2. Access the ShortcutManager and grid context

    const shortcutManager = hot.getShortcutManager();
    const gridContext = shortcutManager.getContext('grid');

    What’s happening:

    • hot.getShortcutManager() returns the ShortcutManager instance attached to this grid.
    • shortcutManager.getContext('grid') returns the 'grid' context — the context that is active whenever the grid has keyboard focus. Shortcuts registered here fire only while the user is interacting with the grid.

    Why a context? Handsontable uses contexts to scope shortcuts. The 'grid' context is the default active context when the grid has focus. Other contexts (for example 'editor') become active when a cell editor is open, so your custom grid shortcuts do not interfere with text input.

  3. Register Ctrl+D to duplicate a row

    gridContext.addShortcut({
    keys: [['Control', 'd']],
    group: 'customActions',
    runOnlyIf: () => hot.getSelected() !== undefined,
    callback: (event) => {
    event.preventDefault();
    const selectedRange = hot.getSelectedRangeLast();
    if (!selectedRange) {
    return;
    }
    const row = selectedRange.from.row;
    const rowData = hot.getSourceDataAtRow(row);
    hot.alter('insert_row_below', row);
    hot.populateFromArray(row + 1, 0, [Object.values(rowData)]);
    lastShortcutEl.textContent = 'Ctrl+D -- row duplicated';
    },
    });

    What’s happening:

    • keys: [['Control', 'd']] — an array of key combinations. Each inner array is one combination; multiple inner arrays mean “any of these combinations”. Key names match the KeyboardEvent.key property (case-insensitive, with Control, Alt, Shift, and Meta as modifiers).
    • group: 'customActions' — a logical group name. Groups let you remove or disable multiple shortcuts at once.
    • runOnlyIf: () => hot.getSelected() !== undefined — a guard function. The callback fires only when this function returns true. Here it ensures at least one cell is selected before the shortcut does anything.
    • event.preventDefault() — prevents the browser’s default behavior for Ctrl+D (which bookmarks the page in some browsers).
    • hot.getSelectedRangeLast() — returns a CellRange describing the last selection. .from.row gives the top row of that selection (visual index).
    • hot.getSourceDataAtRow(row) — reads the raw source data object for that row, bypassing any column sorting or filtering.
    • hot.alter('insert_row_below', row) — inserts a new empty row immediately below the selected row.
    • hot.populateFromArray(row + 1, 0, [Object.values(rowData)]) — fills the new row with the original row’s values. populateFromArray expects a 2-D array, so wrap the flat values array in an outer array.
  4. Register Ctrl+Enter to submit the grid data

    gridContext.addShortcut({
    keys: [['Control', 'Enter']],
    group: 'customActions',
    runOnlyIf: () => true,
    callback: (event) => {
    event.preventDefault();
    const data = hot.getData();
    const headers = hot.getColHeader();
    const rowCount = data.length;
    const timestamp = new Date().toLocaleTimeString();
    lastShortcutEl.textContent = 'Ctrl+Enter -- data submitted';
    lastSubmissionEl.textContent = `[${timestamp}] Submitted ${rowCount} rows -- columns: ${headers.join(', ')}`;
    },
    });

    Where lastShortcutEl and lastSubmissionEl are references to the <code> cells in the shortcut log table. Acquire them once after Handsontable initializes:

    const preview = container.closest('.hot-example-preview') ?? container.parentElement;
    const lastShortcutEl = preview.querySelector('.last-shortcut');
    const lastSubmissionEl = preview.querySelector('.last-submission');

    What’s happening:

    • keys: [['Control', 'Enter']] — fires on Ctrl+Enter. In a standard HTML form, Enter submits, but inside a data grid you often want to capture Ctrl+Enter for a deliberate “submit all” action.
    • event.preventDefault() — in some browsers Ctrl+Enter can trigger form submission or other default behavior; this prevents it.
    • hot.getData() — returns a 2-D array of all visible cell values (respecting column and row order after sorting or filtering).
    • hot.getColHeader() — returns the current column header labels as an array.
    • The summary is written to the shortcut log table below the grid. In a real application you would replace this with a fetch() call or a Redux action.
  5. (Optional) Remove a built-in shortcut

    If a built-in shortcut conflicts with your custom one, remove it before registering yours:

    // Remove the built-in Delete key shortcut (clears cell content)
    shortcutManager.getContext('grid').removeShortcutsByKeys([['Delete']]);

    Why use removeShortcutsByKeys instead of overwriting? Handsontable checks for key conflicts at registration time. Removing the built-in shortcut first prevents a console warning and ensures only your handler runs.

    The table below lists the most commonly overridden built-in shortcuts:

    KeysDefault action
    DeleteClear cell content
    BackspaceClear cell content
    EnterOpen cell editor / move down
    TabMove focus to next cell
    EscapeCancel editing
    Control+ASelect all cells
    Control+ZUndo
    Control+YRedo
    Control+CCopy selection
    Control+VPaste

How It Works - Complete Flow

  1. Grid rendersShortcutManager initializes with the 'grid' context and all built-in shortcuts already registered.
  2. User focuses the grid — the 'grid' context becomes active; your custom shortcuts are now listening.
  3. User presses Ctrl+DShortcutManager matches the key combination against registered handlers. The runOnlyIf guard runs first. If a row is selected, the callback fires: the selected row is read, a new row is inserted below, and the copy is populated.
  4. User presses Ctrl+EnterShortcutManager matches Ctrl+Enter, calls event.preventDefault(), reads hot.getData(), and writes a summary to the shortcut log table.
  5. User focuses outside the grid — the 'grid' context deactivates; your shortcuts no longer fire.

What you learned

  • How to retrieve the ShortcutManager with hot.getShortcutManager().
  • How to target the 'grid' context with shortcutManager.getContext('grid').
  • How to register a custom shortcut with addShortcut({ keys, group, runOnlyIf, callback }).
  • How to guard shortcuts with runOnlyIf so they only fire when conditions are met.
  • How to duplicate a row using hot.alter() and hot.populateFromArray().
  • How to collect grid data with hot.getData() for a submit action.
  • How to prevent browser defaults with event.preventDefault().
  • How to remove a built-in shortcut with removeShortcutsByKeys().

Next steps

  • Explore the full Keyboard shortcuts guide to see all built-in shortcuts.
  • Use context.addShortcut with keys: [['Control', 'z'], ['Meta', 'z']] to support both Windows (Ctrl) and macOS (Cmd) modifiers in a single registration.
  • Register shortcuts in the 'editor' context to intercept key presses while a cell is being edited.
  • Combine runOnlyIf with hot.isEmptyRow() or custom selection checks to build context-sensitive shortcut menus.