Skip to content

Cell editor

Create custom cell editors to fully control how values are entered in your data grid.

Each cell can have one editor — a class that manages the editor’s DOM element, its value, and the lifecycle from opening to saving. Handsontable’s EditorManager selects and drives the editor automatically. You create editors by extending BaseEditor or any of the built-in editor classes.

Component-based editors

Use React components as editors with the useHotEditor hook. The hook returns value, setValue, and finishEditing, plus callbacks for the editor lifecycle (onOpen, onClose, onPrepare, onFocus).

To prevent the editor from closing when clicked (due to outsideClickDeselects), call event.stopPropagation() on the mousedown event of the editor’s root element.

JavaScript
import { useRef } from 'react';
import { HotTable, HotColumn, useHotEditor } from '@handsontable/react-wrapper';
const EditorComponent = () => {
const mainElementRef = useRef(null);
const { value, setValue, finishEditing } = useHotEditor({
onOpen: () => {
if (!mainElementRef.current) return;
mainElementRef.current.style.display = 'block';
},
onClose: () => {
if (!mainElementRef.current) return;
mainElementRef.current.style.display = 'none';
},
onPrepare: (_row, _column, _prop, TD, _originalValue, _cellProperties) => {
const tdPosition = TD.getBoundingClientRect();
// As the `prepare` method is triggered after selecting
// any cell, we're updating the styles for the editor element,
// so it shows up in the correct position.
if (!mainElementRef.current) return;
mainElementRef.current.style.left = `${tdPosition.left + window.pageXOffset}px`;
mainElementRef.current.style.top = `${tdPosition.top + window.pageYOffset}px`;
},
onFocus: () => {},
});
const setLowerCase = () => {
setValue(value.toString().toLowerCase());
finishEditing();
};
const setUpperCase = () => {
setValue(value.toString().toUpperCase());
finishEditing();
};
const stopMousedownPropagation = (e) => {
e.stopPropagation();
};
const buttonValue = value || '';
return (
<div
id="editorElement"
ref={mainElementRef}
style={{
display: 'none',
position: 'absolute',
left: 0,
top: 0,
background: '#fff',
border: '1px solid #000',
padding: '15px',
zIndex: 999,
}}
onMouseDown={stopMousedownPropagation}
>
<button onClick={setLowerCase.bind(this)}>{buttonValue.toLowerCase()}</button>
<button onClick={setUpperCase.bind(this)}>{buttonValue.toUpperCase()}</button>
</div>
);
};
const data = [
['Obrien Fischer'],
['Alexandria Gordon'],
['John Stafford'],
['Regina Waters'],
['Kay Bentley'],
['Emerson Drake'],
['Dean Stapleton'],
];
const ExampleComponent = () => {
return (
<HotTable
data={data}
rowHeaders={true}
autoWrapRow={true}
autoWrapCol={true}
height="auto"
licenseKey="non-commercial-and-evaluation"
>
<HotColumn width={250} editor={EditorComponent} />
</HotTable>
);
};
export default ExampleComponent;
TypeScript
import { MouseEvent, useRef } from 'react';
import { HotTable, HotColumn, useHotEditor } from '@handsontable/react-wrapper';
const EditorComponent = () => {
const mainElementRef = useRef<HTMLDivElement>(null);
const { value, setValue, finishEditing } = useHotEditor({
onOpen: () => {
if (!mainElementRef.current) return;
mainElementRef.current.style.display = 'block';
},
onClose: () => {
if (!mainElementRef.current) return;
mainElementRef.current.style.display = 'none';
},
onPrepare: (_row, _column, _prop, TD, _originalValue, _cellProperties) => {
const tdPosition = TD.getBoundingClientRect();
// As the `prepare` method is triggered after selecting
// any cell, we're updating the styles for the editor element,
// so it shows up in the correct position.
if (!mainElementRef.current) return;
mainElementRef.current.style.left = `${tdPosition.left + window.pageXOffset}px`;
mainElementRef.current.style.top = `${tdPosition.top + window.pageYOffset}px`;
},
onFocus: () => {},
});
const setLowerCase = () => {
setValue(value.toString().toLowerCase());
finishEditing();
};
const setUpperCase = () => {
setValue(value.toString().toUpperCase());
finishEditing();
};
const stopMousedownPropagation = (e: MouseEvent) => {
e.stopPropagation();
};
const buttonValue = value || '';
return (
<div
id="editorElement"
ref={mainElementRef}
style={{
display: 'none',
position: 'absolute',
left: 0,
top: 0,
background: '#fff',
border: '1px solid #000',
padding: '15px',
zIndex: 999,
}}
onMouseDown={stopMousedownPropagation}
>
<button onClick={setLowerCase.bind(this)}>{buttonValue.toLowerCase()}</button>
<button onClick={setUpperCase.bind(this)}>{buttonValue.toUpperCase()}</button>
</div>
);
};
const data = [
['Obrien Fischer'],
['Alexandria Gordon'],
['John Stafford'],
['Regina Waters'],
['Kay Bentley'],
['Emerson Drake'],
['Dean Stapleton'],
];
const ExampleComponent = () => {
return (
<HotTable
data={data}
rowHeaders={true}
autoWrapRow={true}
autoWrapCol={true}
height="auto"
licenseKey="non-commercial-and-evaluation"
>
<HotColumn width={250} editor={EditorComponent} />
</HotTable>
);
};
export default ExampleComponent;

Class-based editors

Declare the editor as a class that extends a built-in editor (e.g., TextEditor) and pass it to the editor option in column configuration.

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { TextEditor } from 'handsontable/editors/textEditor';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
class CustomEditor extends TextEditor {
createElements() {
super.createElements();
this.TEXTAREA = document.createElement('input');
this.TEXTAREA.setAttribute('placeholder', 'Custom placeholder');
this.TEXTAREA.setAttribute('data-hot-input', 'true');
this.textareaStyle = this.TEXTAREA.style;
this.TEXTAREA_PARENT.innerText = '';
this.TEXTAREA_PARENT.appendChild(this.TEXTAREA);
}
}
const ExampleComponent = () => {
return (
<HotTable
id="hot"
startRows={5}
columns={[
{
editor: CustomEditor,
},
]}
colHeaders={true}
colWidths={200}
autoWrapRow={true}
autoWrapCol={true}
height="auto"
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { TextEditor } from 'handsontable/editors/textEditor';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
class CustomEditor extends TextEditor {
createElements() {
super.createElements();
this.TEXTAREA = document.createElement('input');
this.TEXTAREA.setAttribute('placeholder', 'Custom placeholder');
this.TEXTAREA.setAttribute('data-hot-input', 'true');
this.textareaStyle = this.TEXTAREA.style;
this.TEXTAREA_PARENT.innerText = '';
this.TEXTAREA_PARENT.appendChild(this.TEXTAREA);
}
}
const ExampleComponent = () => {
return (
<HotTable
id="hot"
startRows={5}
columns={[
{
editor: CustomEditor,
},
]}
colHeaders={true}
colWidths={200}
autoWrapRow={true}
autoWrapCol={true}
height="auto"
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;

How editing works

Handsontable separates rendering (displaying cell values) from editing (changing them). Renderers are stateless functions; editors are stateful classes that own a DOM element and manage the full editing flow.

EditorManager lifecycle

EditorManager orchestrates all editors. For each cell interaction it runs four steps in order:

StepWhen it runsWhat it does
Select editorCell is selectedLooks up the editor class from the editor config option
PrepareCell is selectedCalls prepare() to configure the editor for the selected cell
OpenUser triggers editingFires beforeBeginEditing (return false to cancel), then calls beginEditing()open()
CloseUser confirms or cancelsCalls finishEditing()close() or focus()

Editing opens on: Enter, Shift+Enter, F2, double-click.

Preventing the editor from opening: Use the beforeBeginEditing hook to conditionally cancel editor opening. Return false from the hook callback to prevent the editor from opening. Returning undefined (or any non-boolean value) applies the default behavior, which disallows opening for non-contiguous selections (Ctrl/Cmd+click) and multi-cell selections (Shift+click). Returning true removes those restrictions.

Editing closes on:

Key / actionResult
Click another cellSaves changes
Enter / Shift+EnterSaves and moves selection down / up
Tab / Shift+TabSaves and moves selection right / left
Ctrl/Cmd+Enter or Alt/Option+EnterInserts a line break
Page Up / Page DownSaves and scrolls one screen
EscapeCancels without saving

Overriding keyboard behavior: Register a beforeKeyDown hook listener and call event.stopImmediatePropagation() to prevent EditorManager from processing a specific key. Register the listener in open() and remove it in close() so it only intercepts events while your editor is active.

Editor singleton: Each editor class has exactly one instance per table. The constructor and init() run once per table; prepare() runs every time the user selects a cell that uses that editor.

BaseEditor API

All custom editors extend Handsontable.editors.BaseEditor. In an ESM project, import it from handsontable/editors/baseEditor. In a script-tag (UMD) project, access it as Handsontable.editors.BaseEditor.

Common methods

These methods are already implemented in BaseEditor. Override them only when necessary — always call super.method() first when you do.

MethodDescription
prepare()Configures the editor for the selected cell. Sets this.row, this.col, this.prop, this.TD, and this.cellProperties. Parameters: (row, col, prop, td, originalValue, cellProperties).
beginEditing()Sets the initial editor value and calls open(). Uses the original cell value when newInitialValue is undefined. Parameters: (newInitialValue, event).
finishEditing()Validates and saves the value, or restores the original when restoreOriginalValue is true. When ctrlDown is true, applies the value to all selected cells. Calls close() on success or focus() when validation fails. Parameters: (restoreOriginalValue?, ctrlDown?, callback?).
discardEditor()Called after validation. If valid, calls close(); if invalid, calls focus() to keep the editor open. Parameter: (result).
saveValue()Validates and saves value. Applies to all selected cells when ctrlDown is true. Parameters: (value, ctrlDown).
isOpened()Returns true if the editor is currently open.
extend()Returns a new subclass of the current editor. Overriding methods on the returned class does not affect the original. Useful for ES5 environments; in ES6+ prefer class MyEditor extends BaseEditor {}.

Abstract methods

You must implement all of these in your custom editor class.

MethodDescription
init()Called once when the editor object is created. Build the editor’s DOM here.
getValue()Returns the current editor value to be saved as the new cell value.
setValue()Updates the editor to display the new value. Parameter: (newValue).
open()Shows the editor.
close()Hides the editor after editing ends.
focus()Focuses the editor input. Called when validation fails and the editor must stay open.

Editor instance properties

Available as this.property inside any editor method.

PropertyTypeDescription
hotHandsontable.CoreThe Handsontable instance this editor belongs to. Set once in the constructor; never changes.
rownumberVisual row index of the active cell. Updated by prepare().
colnumberVisual column index of the active cell. Updated by prepare().
propstringData property name for the active cell (for object data sources). Updated by prepare().
TDHTMLTableCellElementDOM node of the active cell. Updated by prepare().
cellPropertiesobjectConfiguration of the active cell. Updated by prepare(). Includes all standard cell options and any custom properties (e.g., selectOptions).

Built-in editors

AliasEditor class
autocompleteHandsontable.editors.AutocompleteEditor
checkboxHandsontable.editors.CheckboxEditor
dateHandsontable.editors.DateEditor
dropdownHandsontable.editors.DropdownEditor
handsontableHandsontable.editors.HandsontableEditor
numericHandsontable.editors.NumericEditor
passwordHandsontable.editors.PasswordEditor
selectHandsontable.editors.SelectEditor
textHandsontable.editors.TextEditor
timeHandsontable.editors.TimeEditor

Creating a custom editor

Two approaches are available:

  • Extend an existing editor when you want most of its behavior and only need to change a few details (e.g., swap the input element type).
  • Extend BaseEditor directly when you need an entirely different UI (e.g., a dropdown list, a date picker, a color picker).

Extending an existing editor

Override only the methods you need. The PasswordEditor below extends TextEditor and replaces the <textarea> with <input type="password">. Since both elements share the same DOM API, only createElements() needs to change.

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { TextEditor } from 'handsontable/editors/textEditor';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
class CustomEditor extends TextEditor {
createElements() {
super.createElements();
this.TEXTAREA = document.createElement('input');
this.TEXTAREA.setAttribute('placeholder', 'Custom placeholder');
this.TEXTAREA.setAttribute('data-hot-input', 'true');
this.textareaStyle = this.TEXTAREA.style;
this.TEXTAREA_PARENT.innerText = '';
this.TEXTAREA_PARENT.appendChild(this.TEXTAREA);
}
}
const ExampleComponent = () => {
return (
<HotTable
id="hot"
startRows={5}
columns={[
{
editor: CustomEditor,
},
]}
colHeaders={true}
colWidths={200}
autoWrapRow={true}
autoWrapCol={true}
height="auto"
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { TextEditor } from 'handsontable/editors/textEditor';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
class CustomEditor extends TextEditor {
createElements() {
super.createElements();
this.TEXTAREA = document.createElement('input');
this.TEXTAREA.setAttribute('placeholder', 'Custom placeholder');
this.TEXTAREA.setAttribute('data-hot-input', 'true');
this.textareaStyle = this.TEXTAREA.style;
this.TEXTAREA_PARENT.innerText = '';
this.TEXTAREA_PARENT.appendChild(this.TEXTAREA);
}
}
const ExampleComponent = () => {
return (
<HotTable
id="hot"
startRows={5}
columns={[
{
editor: CustomEditor,
},
]}
colHeaders={true}
colWidths={200}
autoWrapRow={true}
autoWrapCol={true}
height="auto"
licenseKey="non-commercial-and-evaluation"
/>
);
};
export default ExampleComponent;

Building an editor from scratch

Extend BaseEditor directly for full control. The SelectEditor below renders a <select> dropdown. It also overrides keyboard behavior so Arrow Up / Arrow Down cycle through options rather than closing the editor.

Key implementation decisions:

  • init() - DOM is created here (runs once), not in prepare() or open().
  • data-hot-input="true" - Required on the editor element. Without it, Handsontable treats clicks on the editor as clicks outside it and closes the editor immediately.
  • prepare() - Options are populated here so each cell can have its own list via selectOptions.
  • open() / close() - The beforeKeyDown hook is registered when opening and cleared when closing, scoping key interception to this editor only.

To use a class-based editor from scratch in React, pass the editor class as the editor prop in a column configuration object. See the class-based editors section for an example.

Registering an editor

Register your editor under a string alias so it can be referenced by name in configuration instead of passing the class directly:

Handsontable.editors.registerEditor('myEditor', MyEditor);

After registration, use the alias in column configuration:

<HotTable columns={[{ editor: 'myEditor' }]} />

To reuse the same editor component across multiple columns, import and pass it wherever it is needed:

import { MySelectEditor } from './MySelectEditor';
<HotTable>
<HotColumn editor={MySelectEditor} />
<HotColumn editor={MySelectEditor} />
</HotTable>

Handsontable defines these aliases by default: autocomplete, base, checkbox, date, dropdown, handsontable, numeric, password, select, text, time.

Choose a unique alias (e.g., 'myorg.select') to avoid overwriting built-in editors. Registering under an existing alias silently overwrites the previous mapping.

Publishing a custom editor

If you intend to distribute your editor as a library, two extra steps make it easy for others to discover and extend it.

Step 1: Register the alias (covered above) so users can reference the editor by name:

Handsontable.editors.registerEditor('myorg.select', MySelectEditor);

Step 2: Expose the class on Handsontable.editors so users can retrieve and extend it without importing from your source module:

Handsontable.editors.MySelectEditor = MySelectEditor;

To extend a published editor, retrieve it with getEditor():

const MySelectEditor = Handsontable.editors.getEditor('myorg.select');
class ExtendedSelectEditor extends MySelectEditor {
// override methods as needed
}
WindowsmacOSActionExcelSheets
Arrow keysArrow keysMove the cursor through the text
Alphanumeric keysAlphanumeric keysEnter the pressed key’s value into the cell
EnterEnterComplete the cell entry and move to the cell below
Shift+EnterShift+EnterComplete the cell entry and move to the cell above
TabTabComplete the cell entry and move to the next cell*
Shift+TabShift+TabComplete the cell entry and move to the previous cell*
DeleteDeleteDelete one character after the cursor*
BackspaceBackspaceDelete one character before the cursor*
HomeHomeMove the cursor to the beginning of the text*
EndEndMove the cursor to the end of the text*
Ctrl + Arrow keysCmd + Arrow keysMove the cursor to the beginning or to the end of the text
Ctrl+Shift + Arrow keysCmd+Shift + Arrow keysExtend the selection to the beginning or to the end of the text
Page UpPage UpComplete the cell entry and move one screen up
Page DownPage DownComplete the cell entry and move one screen down
Alt+EnterOption+EnterInsert a line break
Ctrl+EnterCtrl/Cmd+EnterInsert a line break
EscapeEscapeCancel the cell entry and exit the editing mode

* This action depends on your layout direction.

APIs

Configuration options

Core methods

Hooks