Custom keyboard shortcuts
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.
/* file: app.component.ts */import { Component, ViewChild, AfterViewInit, OnDestroy, NgZone, inject } from '@angular/core';import { GridSettings, HotTableComponent, HotTableModule } from '@handsontable/angular-wrapper';
/* 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 */
@Component({ selector: 'example1-keyboard-shortcuts', standalone: true, imports: [HotTableModule], template: ` <hot-table [data]="data" [settings]="gridSettings"></hot-table> <strong>Shortcut log:</strong> <table class="debug-table"> <colgroup> <col style="width: 180px"> <col> </colgroup> <tbody> <tr> <td>Last shortcut triggered</td> <td><code>{{ lastShortcut }}</code></td> </tr> <tr> <td>Last submission</td> <td><code>{{ lastSubmission }}</code></td> </tr> </tbody> </table> `,})export class AppComponent implements AfterViewInit, OnDestroy { @ViewChild(HotTableComponent, { static: false }) readonly hotTable!: HotTableComponent;
private readonly ngZone = inject(NgZone);
data = employees; lastShortcut = '—'; lastSubmission = '—';
readonly gridSettings: GridSettings = { 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, };
ngAfterViewInit(): void { const hot = this.hotTable?.hotInstance;
if (!hot) { return; }
const gridContext = hot.getShortcutManager().getContext('grid');
if (!gridContext) { return; }
// Ctrl+D: duplicate the currently selected row gridContext.addShortcut({ keys: [['Control', 'd']], group: 'customActions', runOnlyIf: () => hot.getSelected() !== undefined, callback: (event: Event) => { event.preventDefault();
const selectedRange = hot.getSelectedRangeLast();
if (!selectedRange) { return; }
const row = selectedRange.from.row; const rowData = hot.getSourceDataAtRow(row) as Record<string, unknown>;
hot.alter('insert_row_below', row); hot.populateFromArray(row + 1, 0, [Object.values(rowData)]);
this.ngZone.run(() => { this.lastShortcut = 'Ctrl+D -- row duplicated'; }); }, });
// Ctrl+Enter: submit the grid data gridContext.addShortcut({ keys: [['Control', 'Enter']], group: 'customActions', runOnlyIf: () => true, callback: (event: Event) => { event.preventDefault();
const data = hot.getData(); const headers = hot.getColHeader() as string[]; const rowCount = data.length; const timestamp = new Date().toLocaleTimeString();
this.ngZone.run(() => { this.lastShortcut = 'Ctrl+Enter -- data submitted'; this.lastSubmission = `[${timestamp}] Submitted ${rowCount} rows -- columns: ${headers.join(', ')}`; }); }, }); }
ngOnDestroy(): void { const hot = this.hotTable?.hotInstance;
if (!hot) { return; }
hot.getShortcutManager().getContext('grid')?.removeShortcutsByGroup('customActions'); }}/* end-file */
/* file: app.config.ts */import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';import { registerAllModules } from 'handsontable/registry';import { HOT_GLOBAL_CONFIG, HotGlobalConfig, NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
registerAllModules();
export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), { provide: HOT_GLOBAL_CONFIG, useValue: { license: NON_COMMERCIAL_LICENSE } as HotGlobalConfig, }, ],};/* end-file */<div> <example1-keyboard-shortcuts></example1-keyboard-shortcuts></div>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()andhot.getSourceDataAtRow(). - Modifying rows with
hot.alter()andhot.populateFromArray().
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 theShortcutManager.- The grid uses an array of objects as its data source. Each property maps to a column via the
datakey.
Access the ShortcutManager and grid context
const shortcutManager = hot.getShortcutManager();const gridContext = shortcutManager.getContext('grid');What’s happening:
hot.getShortcutManager()returns theShortcutManagerinstance 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.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 theKeyboardEvent.keyproperty (case-insensitive, withControl,Alt,Shift, andMetaas 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 returnstrue. Here it ensures at least one cell is selected before the shortcut does anything.event.preventDefault()— prevents the browser’s default behavior forCtrl+D(which bookmarks the page in some browsers).hot.getSelectedRangeLast()— returns aCellRangedescribing the last selection..from.rowgives 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.populateFromArrayexpects a 2-D array, so wrap the flat values array in an outer array.
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
lastShortcutElandlastSubmissionElare 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 onCtrl+Enter. In a standard HTML form,Entersubmits, but inside a data grid you often want to captureCtrl+Enterfor a deliberate “submit all” action.event.preventDefault()— in some browsersCtrl+Entercan 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.
(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
removeShortcutsByKeysinstead 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:
Keys Default 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
- Grid renders —
ShortcutManagerinitializes with the'grid'context and all built-in shortcuts already registered. - User focuses the grid — the
'grid'context becomes active; your custom shortcuts are now listening. - User presses Ctrl+D —
ShortcutManagermatches the key combination against registered handlers. TherunOnlyIfguard 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. - User presses Ctrl+Enter —
ShortcutManagermatchesCtrl+Enter, callsevent.preventDefault(), readshot.getData(), and writes a summary to the shortcut log table. - User focuses outside the grid — the
'grid'context deactivates; your shortcuts no longer fire.
What you learned
- How to retrieve the
ShortcutManagerwithhot.getShortcutManager(). - How to target the
'grid'context withshortcutManager.getContext('grid'). - How to register a custom shortcut with
addShortcut({ keys, group, runOnlyIf, callback }). - How to guard shortcuts with
runOnlyIfso they only fire when conditions are met. - How to duplicate a row using
hot.alter()andhot.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.addShortcutwithkeys: [['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
runOnlyIfwithhot.isEmptyRow()or custom selection checks to build context-sensitive shortcut menus.