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)
settings: GridSettings = {
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.
TypeScript
/* file: app.component.ts */
import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { GridSettings } from '@handsontable/angular-wrapper';
import Handsontable from 'handsontable/base';
// Custom renderer: visualizes stock level as a progress bar with a numeric label.
// Demonstrates using a renderer independently from the editor and validator.
const stockRenderer = (
hotInstance: Handsontable.Core,
td: HTMLTableCellElement,
_row: number,
_col: number,
_prop: string | number,
value: Handsontable.CellValue
): 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.
const stockValidator = (value: Handsontable.CellValue, callback: (valid: boolean) => void): void => {
const num = Number(value);
callback(Number.isInteger(num) && num >= 0 && num <= 1000);
};
@Component({
selector: 'app-example1',
template: `
<hot-table [settings]="hotSettings!" [data]="hotData"></hot-table>
`,
// ViewEncapsulation.None makes these styles global so they apply to DOM
// elements created by the Handsontable renderer outside Angular's component tree.
styles: [`
.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;
}
`],
encapsulation: ViewEncapsulation.None,
standalone: false,
})
export class AppComponent implements OnInit {
readonly hotData = [
['Apple', 1.2, 820],
['Banana', 0.5, 280],
['Cherry', 3.0, 45],
['Mango', 2.5, 960],
['Pear', 0.8, 170],
['Blueberry', 4.5, 15],
];
hotSettings!: GridSettings;
ngOnInit() {
this.hotSettings = {
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,
};
}
}
/* end-file */
/* file: app.module.ts */
import { NgModule, ApplicationConfig } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { registerAllModules } from 'handsontable/registry';
import { HOT_GLOBAL_CONFIG, HotGlobalConfig, HotTableModule } from '@handsontable/angular-wrapper';
import { CommonModule } from '@angular/common';
import { NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
/* start:skip-in-compilation */
import { AppComponent } from './app.component';
/* end:skip-in-compilation */
// register Handsontable's modules
registerAllModules();
export const appConfig: ApplicationConfig = {
providers: [
{
provide: HOT_GLOBAL_CONFIG,
useValue: {
license: NON_COMMERCIAL_LICENSE,
} as HotGlobalConfig
}
],
};
@NgModule({
imports: [BrowserModule, HotTableModule, CommonModule],
declarations: [AppComponent],
providers: [...appConfig.providers],
bootstrap: [AppComponent]
})
export class AppModule { }
/* end-file */
HTML
<div>
<app-example1></app-example1>
</div>

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 = this.hotTable.hotInstance.getCellMeta(0, 0);
const renderer = cellProperties.renderer; // renderer function
const editor = cellProperties.editor; // editor class
const validator = cellProperties.validator; // validator function or RegExp
const type = 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:

import { Component, AfterViewInit, ViewChild } from '@angular/core';
import { HotTableComponent } from '@handsontable/angular-wrapper';
@Component({
selector: 'app-example',
template: '<hot-table [settings]="settings"></hot-table>',
standalone: false,
})
export class AppComponent implements AfterViewInit {
@ViewChild(HotTableComponent) hotTable!: HotTableComponent;
settings = {
columns: [{ type: 'numeric' }],
};
ngAfterViewInit() {
const hot = this.hotTable.hotInstance;
const cellProperties = hot.getCellMeta(0, 0);
const renderer = cellProperties.renderer; // numericRenderer function
const editor = cellProperties.editor; // NumericEditor class
const validator = cellProperties.validator; // numericValidator function
const type = cellProperties.type; // 'numeric'
}
}

Related guides

Configuration options