Skip to content

Cell functions

Render, edit, and validate cell contents using Handsontable’s three independent cell functions.

Overview

Every Handsontable cell has three associated functions that handle distinct concerns:

FunctionRoleImplemented as
rendererControls how a cell looks: DOM structure, CSS classes, HTML contentA plain function
editorControls how a cell is edited: input element, keyboard handling, open/close lifecycleA class extending BaseEditor
validatorDecides whether a cell value is acceptableA function or RegExp

The three functions are independent. You can mix and match any combination: use the built-in numeric editor with a custom renderer, override just the validator while keeping a built-in type, or write all three from scratch.

Function signatures

// renderer — called for every visible cell on every render
renderer(hotInstance, td, row, col, prop, value, cellProperties)
// hotInstance – Handsontable instance
// td – HTMLTableCellElement to modify
// row, col – visual row and column indexes
// prop – data property name (string) or column index (number)
// value – current cell value
// cellProperties – merged cell configuration object
// validator — may be synchronous or asynchronous
validator(value, callback)
// value – value to validate
// callback – call with true (valid) or false (invalid)
// RegExp alternative: /pattern/.test(value) must return true
// editor — a class; see the Cell editor guide for the full lifecycle API
class MyEditor extends BaseEditor { ... }

validator is optional. If no validator is defined for a cell, the cell is skipped entirely during validation — afterValidate will not fire for it, and it will not contribute to the validation cycle.

allowInvalid

By default, allowInvalid: true — invalid cells are accepted into the data source but marked with the htInvalid CSS class. Set allowInvalid: false to reject invalid values and keep the editor open until a valid value is entered.

Cell types bundle all three

A cell type is a preset that assigns a matching renderer, editor, and validator together under a single type alias. Using type: 'numeric' is shorthand for:

{
renderer: Handsontable.renderers.NumericRenderer,
editor: Handsontable.editors.NumericEditor,
validator: Handsontable.validators.NumericValidator,
}

Built-in types: text, numeric, checkbox, date, time, dropdown, autocomplete, password, handsontable.

When you set an explicit renderer, editor, or validator alongside a type, the explicit function always takes precedence over the type for that function only:

columns: [{
type: 'numeric', // sets NumericEditor + NumericValidator
renderer: myRenderer, // overrides only NumericRenderer; editor and validator stay numeric
}]

When to use a type vs individual functions:

  • Use type when you want the standard, bundled behavior for a data kind (numbers, dates, checkboxes).
  • Override a single function from a type when one aspect needs customizing but the rest is fine as-is.
  • Set individual renderer/editor/validator directly when no built-in type fits or you need full control.

Configuration priority

Cell functions resolve using the cascading configuration model. The most specific level wins:

cell[row][col] > column > global (root settings)
new Handsontable(container, {
type: 'text', // global fallback for all cells
columns: [
{ type: 'numeric' }, // overrides global for all cells in column 0
{ type: 'text' }, // same as global for column 1
],
cell: [
{ row: 0, col: 0, type: 'checkbox' }, // overrides column setting for cell [0, 0] only
],
});

Mixing renderer, editor, and validator

The example below shows a product inventory table. Each column uses a different function configuration:

  • Producttype: 'text' bundles text renderer, text editor, and no validator.
  • Pricetype: 'numeric' bundles numeric renderer (formatted as currency), numeric editor, and numeric validator.
  • Stock — custom renderer (progress bar), built-in 'numeric' editor, and a custom range validator. All three come from different sources.
JavaScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
registerAllModules();
// Custom renderer: visualizes stock level as a progress bar with a numeric label.
// Demonstrates using a renderer independently from the editor and validator.
function stockRenderer(hotInstance, td, row, col, prop, value) {
const num = parseInt(value, 10);
const valid = !isNaN(num) && num >= 0;
const pct = valid ? Math.min(100, (num / 1000) * 100) : 0;
const color = pct > 60 ? '#22c55e' : pct > 20 ? '#f59e0b' : '#ef4444';
td.innerText = '';
const wrapper = hotInstance.rootDocument.createElement('div');
wrapper.className = 'htStockBar';
const track = hotInstance.rootDocument.createElement('div');
track.className = 'htStockBarTrack';
const fill = hotInstance.rootDocument.createElement('div');
fill.className = 'htStockBarFill';
fill.style.width = `${pct}%`;
fill.style.background = color;
const label = hotInstance.rootDocument.createElement('span');
label.className = 'htStockBarLabel';
label.innerText = valid ? `${num}` : '—';
track.appendChild(fill);
wrapper.appendChild(track);
wrapper.appendChild(label);
td.appendChild(wrapper);
return td;
}
// Custom validator: accepts integers in the range 0–1000.
// Demonstrates using a validator independently from the renderer and editor.
function stockValidator(value, callback) {
const num = Number(value);
callback(Number.isInteger(num) && num >= 0 && num <= 1000);
}
const container = document.querySelector('#example1');
new Handsontable(container, {
data: [
['Apple', 1.2, 820],
['Banana', 0.5, 280],
['Cherry', 3.0, 45],
['Mango', 2.5, 960],
['Pear', 0.8, 170],
['Blueberry', 4.5, 15],
],
colHeaders: ['Product', 'Price', 'Stock'],
columns: [
// Built-in type bundles renderer + editor + no validator
{ type: 'text' },
// Built-in type bundles renderer + editor + validator with custom format
{
type: 'numeric',
locale: 'en-US',
numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 },
},
// Mixed: custom renderer, built-in numeric editor, custom validator
{
renderer: stockRenderer,
editor: 'numeric',
validator: stockValidator,
allowInvalid: false,
},
],
colWidths: [120, 90, 200],
rowHeaders: true,
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
licenseKey: 'non-commercial-and-evaluation',
});
TypeScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
registerAllModules();
// Custom renderer: visualizes stock level as a progress bar with a numeric label.
// Demonstrates using a renderer independently from the editor and validator.
function stockRenderer(
hotInstance: Handsontable.Core,
td: HTMLTableCellElement,
row: number,
col: number,
prop: string | number,
value: any
): HTMLTableCellElement {
const num = parseInt(value as string, 10);
const valid = !isNaN(num) && num >= 0;
const pct = valid ? Math.min(100, (num / 1000) * 100) : 0;
const color = pct > 60 ? '#22c55e' : pct > 20 ? '#f59e0b' : '#ef4444';
td.innerText = '';
const wrapper = hotInstance.rootDocument.createElement('div');
wrapper.className = 'htStockBar';
const track = hotInstance.rootDocument.createElement('div');
track.className = 'htStockBarTrack';
const fill = hotInstance.rootDocument.createElement('div');
fill.className = 'htStockBarFill';
fill.style.width = `${pct}%`;
fill.style.background = color;
const label = hotInstance.rootDocument.createElement('span');
label.className = 'htStockBarLabel';
label.innerText = valid ? `${num}` : '—';
track.appendChild(fill);
wrapper.appendChild(track);
wrapper.appendChild(label);
td.appendChild(wrapper);
return td;
}
// Custom validator: accepts integers in the range 0–1000.
// Demonstrates using a validator independently from the renderer and editor.
function stockValidator(value: any, callback: (valid: boolean) => void): void {
const num = Number(value);
callback(Number.isInteger(num) && num >= 0 && num <= 1000);
}
const container = document.querySelector('#example1') as HTMLElement;
new Handsontable(container, {
data: [
['Apple', 1.2, 820],
['Banana', 0.5, 280],
['Cherry', 3.0, 45],
['Mango', 2.5, 960],
['Pear', 0.8, 170],
['Blueberry', 4.5, 15],
],
colHeaders: ['Product', 'Price', 'Stock'],
columns: [
// Built-in type bundles renderer + editor + no validator
{ type: 'text' },
// Built-in type bundles renderer + editor + validator with custom format
{ type: 'numeric', locale: 'en-US', numericFormat: { style: 'currency', currency: 'USD', minimumFractionDigits: 2 } },
// Mixed: custom renderer, built-in numeric editor, custom validator
{
renderer: stockRenderer,
editor: 'numeric',
validator: stockValidator,
allowInvalid: false,
} as any,
],
colWidths: [120, 90, 200],
rowHeaders: true,
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
licenseKey: 'non-commercial-and-evaluation',
});
CSS
.htStockBar {
display: flex;
align-items: center;
gap: 6px;
padding: 0 4px;
height: 100%;
box-sizing: border-box;
}
.htStockBarTrack {
flex: 1;
height: 8px;
background: #e5e7eb;
border-radius: 4px;
overflow: hidden;
}
.htStockBarFill {
height: 100%;
border-radius: 4px;
min-width: 2px;
}
.htStockBarLabel {
font-size: 11px;
font-variant-numeric: tabular-nums;
min-width: 28px;
text-align: right;
white-space: nowrap;
}

Double-click any Stock cell to edit it with the numeric editor. The bar renderer updates on save. Enter a value outside 0–1000 to see the validator reject it (cell turns red when allowInvalid: false).

Performance

Renderers are called separately for every displayed cell on every table render. A table can render many times during its lifetime — after scrolling, sorting, editing, and more. Keep renderer functions as simple and fast as possible to avoid performance drops, especially with large datasets.

Getting cell functions programmatically

Use getCellMeta(row, col) to read all properties of a cell at once, or the dedicated getters for individual functions:

const cellProperties = hot.getCellMeta(0, 0);
cellProperties.renderer; // renderer function
cellProperties.editor; // editor class
cellProperties.validator; // validator function or RegExp
cellProperties.type; // cell type string

Dedicated getters:

MethodReturns
getCellRenderer(row, col)The resolved renderer function for the cell
getCellEditor(row, col)The resolved editor class for the cell
getCellValidator(row, col)The resolved validator function or RegExp for the cell

If a cell’s functions are defined through a cell type, the getters return the resolved functions, not the type string:

const hot = new Handsontable(container, {
columns: [{ type: 'numeric' }],
});
const cellProperties = hot.getCellMeta(0, 0);
cellProperties.renderer; // numericRenderer function
cellProperties.editor; // NumericEditor class
cellProperties.validator; // numericValidator function
cellProperties.type; // 'numeric'

Related guides

Configuration options