Skip to content
JavaScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
import { rendererFactory, getRenderer } from 'handsontable/renderers';
import { getEditor } from 'handsontable/editors';
import { getValidator } from 'handsontable/validators';
import { registerCellType } from 'handsontable/cellTypes';
import numbro from 'numbro';
import languages from 'numbro/dist/languages.min.js';
// Register all Handsontable's modules.
registerAllModules();
Object.values(languages).forEach((language) => numbro.registerLanguage(language));
function isNumeric(value) {
const type = typeof value;
if (type === 'number') {
return !isNaN(value) && isFinite(value);
} else if (type === 'string') {
if (value.length === 0) {
return false;
} else if (value.length === 1) {
return /\d/.test(value);
}
const delimiter = Array.from(new Set(['.']))
.map((d) => `\\${d}`)
.join('|');
return new RegExp(`^[+-]?(((${delimiter})?\\d+((${delimiter})\\d+)?(e[+-]?\\d+)?)|(0x[a-f\\d]+))$`, 'i').test(
value.trim()
);
} else if (type === 'object') {
return !!value && typeof value.valueOf() === 'number' && !(value instanceof Date);
}
return false;
}
/* start:skip-in-preview */
const data = [
{
id: 640329,
itemName: 'Lunar Core',
itemNo: 'XJ-12',
leadEngineer: 'Ellen Ripley',
cost: 350000,
inStock: true,
category: 'Lander',
itemQuality: 87,
origin: '🇺🇸 USA',
quantity: 2,
valueStock: 700000,
repairable: false,
supplierName: 'TechNova',
restockDate: '2025-08-01',
operationalStatus: 'Awaiting Parts',
},
{
id: 863104,
itemName: 'Zero Thrusters',
itemNo: 'QL-54',
leadEngineer: 'Sam Bell',
cost: 450000,
inStock: false,
category: 'Propulsion',
itemQuality: 0,
origin: '🇩🇪 Germany',
quantity: 0,
valueStock: 0,
repairable: true,
supplierName: 'PropelMax',
restockDate: '2025-09-15',
operationalStatus: 'In Maintenance',
},
{
id: 395603,
itemName: 'EVA Suits',
itemNo: 'PM-67',
leadEngineer: 'Alex Rogan',
cost: 150000,
inStock: true,
category: 'Equipment',
itemQuality: 79,
origin: '🇮🇹 Italy',
quantity: 50,
valueStock: 7500000,
repairable: true,
supplierName: 'SuitCraft',
restockDate: '2025-10-05',
operationalStatus: 'Ready for Testing',
},
{
id: 679083,
itemName: 'Solar Panels',
itemNo: 'BW-09',
leadEngineer: 'Dave Bowman',
cost: 75000,
inStock: true,
category: 'Energy',
itemQuality: 95,
origin: '🇺🇸 USA',
quantity: 10,
valueStock: 750000,
repairable: false,
supplierName: 'SolarStream',
restockDate: '2025-11-10',
operationalStatus: 'Operational',
},
{
id: 912663,
itemName: 'Comm Array',
itemNo: 'ZR-56',
leadEngineer: 'Louise Banks',
cost: 125000,
inStock: false,
category: 'Communication',
itemQuality: 0,
origin: '🇯🇵 Japan',
quantity: 0,
valueStock: 0,
repairable: true,
supplierName: 'CommTech',
restockDate: '2025-12-20',
operationalStatus: 'Decommissioned',
},
{
id: 315806,
itemName: 'Habitat Dome',
itemNo: 'UJ-23',
leadEngineer: 'Dr. Ryan Stone',
cost: 1000000,
inStock: true,
category: 'Shelter',
itemQuality: 93,
origin: '🇨🇦 Canada',
quantity: 3,
valueStock: 3000000,
repairable: false,
supplierName: 'DomeInnovate',
restockDate: '2026-01-25',
operationalStatus: 'Operational',
},
];
/* end:skip-in-preview */
// Get the DOM element with the ID 'example1' where the Handsontable will be rendered
const container = document.querySelector('#example1');
const cellTypeDefinition = {
renderer: rendererFactory(({ hotInstance, td, row, col, prop, value, cellProperties }) => {
if (isNumeric(value)) {
let classArr = [];
if (Array.isArray(cellProperties.className)) {
classArr = cellProperties.className;
} else {
const className = cellProperties.className ?? '';
if (className.length) {
classArr = className.split(' ');
}
}
const numericFormat = cellProperties.numericFormat;
const cellCulture = (numericFormat && numericFormat.culture) || 'en-US';
const cellFormatPattern = numericFormat && numericFormat.pattern;
// Register the language if it's not already registered
if (cellCulture && !numbro.languages()[cellCulture]) {
const shortTag = cellCulture.replace('-', '');
const langData = numbro.allLanguages ? numbro.allLanguages[cellCulture] : numbro[shortTag];
if (langData) {
numbro.registerLanguage(langData);
}
}
numbro.setLanguage(cellCulture);
value = numbro(value).format(cellFormatPattern ?? '0');
if (
classArr.indexOf('htLeft') < 0 &&
classArr.indexOf('htCenter') < 0 &&
classArr.indexOf('htRight') < 0 &&
classArr.indexOf('htJustify') < 0
) {
classArr.push('htRight');
}
if (classArr.indexOf('htNumeric') < 0) {
classArr.push('htNumeric');
}
cellProperties.className = classArr.join(' ');
td.dir = 'ltr';
}
getRenderer('text')(hotInstance, td, row, col, prop, value, cellProperties);
}),
validator: getValidator('numeric'),
editor: getEditor('numeric'),
};
registerCellType('numbro', cellTypeDefinition);
// Define configuration options for the Handsontable
const hotOptions = {
data,
colHeaders: ['Item Name', 'Category', 'Lead Engineer', 'Quantity', 'Cost'],
autoRowSize: true,
rowHeaders: true,
height: 'auto',
width: '100%',
autoWrapRow: true,
headerClassName: 'htLeft',
columns: [
{ data: 'itemName', type: 'text', width: 130 },
{ data: 'category', type: 'text', width: 120 },
{ data: 'leadEngineer', type: 'text', width: 150 },
{
data: 'quantity',
type: 'numbro',
width: 150,
className: 'htRight',
numericFormat: {
pattern: '0,0',
culture: 'en-US',
},
},
{
data: 'cost',
type: 'numbro',
width: 120,
className: 'htRight',
numericFormat: {
pattern: '$0,0.00',
culture: 'en-US',
},
},
],
licenseKey: 'non-commercial-and-evaluation',
};
// Initialize the Handsontable instance with the specified configuration options
// eslint-disable-next-line no-unused-vars
const hot = new Handsontable(container, hotOptions);
TypeScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
import { rendererFactory, getRenderer } from 'handsontable/renderers';
import { getEditor } from 'handsontable/editors';
import { getValidator } from 'handsontable/validators';
import { registerCellType } from 'handsontable/cellTypes';
import numbro from 'numbro';
import languages from 'numbro/dist/languages.min.js';
// Register all Handsontable's modules.
registerAllModules();
Object.values(languages).forEach((language) => numbro.registerLanguage(language));
function isNumeric(value: any): boolean {
const type = typeof value;
if (type === 'number') {
return !isNaN(value) && isFinite(value);
} else if (type === 'string') {
if (value.length === 0) {
return false;
} else if (value.length === 1) {
return /\d/.test(value);
}
const delimiter = Array.from(new Set(['.']))
.map(d => `\\${d}`)
.join('|');
return new RegExp(`^[+-]?(((${delimiter})?\\d+((${delimiter})\\d+)?(e[+-]?\\d+)?)|(0x[a-f\\d]+))$`, 'i')
.test(value.trim());
} else if (type === 'object') {
return !!value && typeof value.valueOf() === 'number' && !(value instanceof Date);
}
return false;
}
/* start:skip-in-preview */
const data = [
{
id: 640329,
itemName: 'Lunar Core',
itemNo: 'XJ-12',
leadEngineer: 'Ellen Ripley',
cost: 350000,
inStock: true,
category: 'Lander',
itemQuality: 87,
origin: '🇺🇸 USA',
quantity: 2,
valueStock: 700000,
repairable: false,
supplierName: 'TechNova',
restockDate: '2025-08-01',
operationalStatus: 'Awaiting Parts',
},
{
id: 863104,
itemName: 'Zero Thrusters',
itemNo: 'QL-54',
leadEngineer: 'Sam Bell',
cost: 450000,
inStock: false,
category: 'Propulsion',
itemQuality: 0,
origin: '🇩🇪 Germany',
quantity: 0,
valueStock: 0,
repairable: true,
supplierName: 'PropelMax',
restockDate: '2025-09-15',
operationalStatus: 'In Maintenance',
},
{
id: 395603,
itemName: 'EVA Suits',
itemNo: 'PM-67',
leadEngineer: 'Alex Rogan',
cost: 150000,
inStock: true,
category: 'Equipment',
itemQuality: 79,
origin: '🇮🇹 Italy',
quantity: 50,
valueStock: 7500000,
repairable: true,
supplierName: 'SuitCraft',
restockDate: '2025-10-05',
operationalStatus: 'Ready for Testing',
},
{
id: 679083,
itemName: 'Solar Panels',
itemNo: 'BW-09',
leadEngineer: 'Dave Bowman',
cost: 75000,
inStock: true,
category: 'Energy',
itemQuality: 95,
origin: '🇺🇸 USA',
quantity: 10,
valueStock: 750000,
repairable: false,
supplierName: 'SolarStream',
restockDate: '2025-11-10',
operationalStatus: 'Operational',
},
{
id: 912663,
itemName: 'Comm Array',
itemNo: 'ZR-56',
leadEngineer: 'Louise Banks',
cost: 125000,
inStock: false,
category: 'Communication',
itemQuality: 0,
origin: '🇯🇵 Japan',
quantity: 0,
valueStock: 0,
repairable: true,
supplierName: 'CommTech',
restockDate: '2025-12-20',
operationalStatus: 'Decommissioned',
},
{
id: 315806,
itemName: 'Habitat Dome',
itemNo: 'UJ-23',
leadEngineer: 'Dr. Ryan Stone',
cost: 1000000,
inStock: true,
category: 'Shelter',
itemQuality: 93,
origin: '🇨🇦 Canada',
quantity: 3,
valueStock: 3000000,
repairable: false,
supplierName: 'DomeInnovate',
restockDate: '2026-01-25',
operationalStatus: 'Operational',
},
];
/* end:skip-in-preview */
// Get the DOM element with the ID 'example1' where the Handsontable will be rendered
const container = document.querySelector('#example1')!;
const cellTypeDefinition = {
renderer: rendererFactory(({ hotInstance, td, row, col, prop, value, cellProperties }) => {
if (isNumeric(value)) {
let classArr = [];
if (Array.isArray(cellProperties.className)) {
classArr = cellProperties.className;
} else {
const className = cellProperties.className ?? '';
if (className.length) {
classArr = className.split(' ');
}
}
const numericFormat = cellProperties.numericFormat;
const cellCulture = numericFormat && numericFormat.culture || 'en-US';
const cellFormatPattern = numericFormat && numericFormat.pattern;
// Register the language if it's not already registered
if (cellCulture && !numbro.languages()[cellCulture]) {
const shortTag = cellCulture.replace('-', '');
const langData = numbro.allLanguages ? numbro.allLanguages[cellCulture] : numbro[shortTag];
if (langData) {
numbro.registerLanguage(langData);
}
}
numbro.setLanguage(cellCulture);
value = numbro(value).format(cellFormatPattern ?? '0');
if (
classArr.indexOf('htLeft') < 0 &&
classArr.indexOf('htCenter') < 0 &&
classArr.indexOf('htRight') < 0 &&
classArr.indexOf('htJustify') < 0
) {
classArr.push('htRight');
}
if (classArr.indexOf('htNumeric') < 0) {
classArr.push('htNumeric');
}
cellProperties.className = classArr.join(' ');
td.dir = 'ltr';
}
getRenderer('text')(hotInstance, td, row, col, prop, value, cellProperties);
}),
validator: getValidator('numeric'),
editor: getEditor('numeric'),
};
registerCellType('numbro', cellTypeDefinition);
// Define configuration options for the Handsontable
const hotOptions: Handsontable.GridSettings = {
data,
colHeaders: ['Item Name', 'Category', 'Lead Engineer', 'Quantity', 'Cost'],
autoRowSize: true,
rowHeaders: true,
height: 'auto',
width: '100%',
autoWrapRow: true,
headerClassName: 'htLeft',
columns: [
{ data: 'itemName', type: 'text', width: 130 },
{ data: 'category', type: 'text', width: 120 },
{ data: 'leadEngineer', type: 'text', width: 150 },
{
data: 'quantity',
type: 'numbro',
width: 150,
className: 'htRight',
numericFormat: {
pattern: '0,0',
culture: 'en-US',
},
},
{
data: 'cost',
type: 'numbro',
width: 120,
className: 'htRight',
numericFormat: {
pattern: '$0,0.00',
culture: 'en-US',
},
},
],
licenseKey: 'non-commercial-and-evaluation',
};
// Initialize the Handsontable instance with the specified configuration options
// eslint-disable-next-line no-unused-vars
const hot = new Handsontable(container, hotOptions);

Overview

This guide shows how to create a custom numbro cell type using the Numbro library. Users can format numbers using the Numbro API.

Difficulty: Beginner Time: ~15 minutes Libraries: numbro

What You’ll Build

A cell that:

  • Displays numbers with locale-aware formatting via Numbro (e.g., $350,000.00)
  • Accepts numericFormat options for per-column formatting customization
  • Validates input using Handsontable’s built-in numeric validator
  • Automatically right-aligns numeric values

Prerequisites

Terminal window
npm install numbro
  1. Import Dependencies

    import Handsontable from 'handsontable/base';
    import { registerAllModules } from 'handsontable/registry';
    import { rendererFactory, getRenderer } from 'handsontable/renderers';
    import { getEditor } from 'handsontable/editors';
    import { getValidator } from 'handsontable/validators';
    import { registerCellType } from 'handsontable/cellTypes';
    import numbro from 'numbro';
    import languages from 'numbro/dist/languages.min.js';
    registerAllModules();
    Object.values(languages).forEach((language) => numbro.registerLanguage(language));

    Why this matters:

    • numbro handles locale-aware number formatting (currencies, decimals, thousands separators)
    • rendererFactory creates a custom renderer that formats values with Numbro before displaying
    • Registering all Numbro languages upfront enables any culture to be used in numericFormat
  2. Create the Numeric Helper

    This helper determines whether a value should be treated as a number:

    function isNumeric(value) {
    const type = typeof value;
    if (type === 'number') {
    return !isNaN(value) && isFinite(value);
    } else if (type === 'string') {
    if (value.length === 0) return false;
    if (value.length === 1) return /\d/.test(value);
    const delimiter = Array.from(new Set(['.']))
    .map(d => `\\${d}`)
    .join('|');
    return new RegExp(
    `^[+-]?(((${delimiter})?\\d+((${delimiter})\\d+)?(e[+-]?\\d+)?)|(0x[a-f\\d]+))$`, 'i'
    ).test(value.trim());
    } else if (type === 'object') {
    return !!value && typeof value.valueOf() === 'number' && !(value instanceof Date);
    }
    return false;
    }
  3. Create the Renderer

    The renderer formats numeric values using Numbro and delegates to the built-in text renderer:

    renderer: rendererFactory(({ hotInstance, td, row, col, prop, value, cellProperties }) => {
    if (isNumeric(value)) {
    const numericFormat = cellProperties.numericFormat;
    const cellCulture = numericFormat && numericFormat.culture || 'en-US';
    const cellFormatPattern = numericFormat && numericFormat.pattern;
    numbro.setLanguage(cellCulture);
    value = numbro(value).format(cellFormatPattern ?? '0');
    // Auto-apply htRight alignment for numeric cells
    td.dir = 'ltr';
    }
    getRenderer('text')(hotInstance, td, row, col, prop, value, cellProperties);
    })

    What’s happening:

    • Reads numericFormat.culture and numericFormat.pattern from cell properties
    • Formats the raw number using numbro(value).format(pattern)
    • Auto-applies htRight alignment unless another alignment class is set
    • Sets td.dir = 'ltr' for correct display in RTL layouts
    • Delegates to the text renderer for final DOM output
  4. Complete Cell Type Definition

    Put all the pieces together and register the cell type:

    const cellTypeDefinition = {
    renderer: rendererFactory(({ hotInstance, td, row, col, prop, value, cellProperties }) => {
    // ... renderer code from Step 3 (see full example above)
    }),
    validator: getValidator('numeric'),
    editor: getEditor('numeric'),
    };
    registerCellType('numbro', cellTypeDefinition);

    What’s happening:

    • renderer: Formats numbers with Numbro and renders as right-aligned text
    • validator: Uses the built-in numeric validator to reject non-numeric input
    • editor: Uses the built-in numeric editor for input
    • registerCellType: Registers the numbro cell type for use in column config
  5. Use in Handsontable

    registerCellType('numbro', cellTypeDefinition);
    const hotOptions: Handsontable.GridSettings = {
    data,
    colHeaders: ['Item Name', 'Category', 'Lead Engineer', 'Quantity', 'Cost'],
    autoRowSize: true,
    rowHeaders: true,
    height: 'auto',
    width: '100%',
    autoWrapRow: true,
    headerClassName: 'htLeft',
    columns: [
    { data: 'itemName', type: 'text', width: 130 },
    { data: 'category', type: 'text', width: 120 },
    { data: 'leadEngineer', type: 'text', width: 150 },
    {
    data: 'quantity',
    type: 'numbro',
    width: 150,
    className: 'htRight',
    numericFormat: {
    pattern: '0,0',
    culture: 'en-US',
    },
    },
    {
    data: 'cost',
    type: 'numbro',
    width: 120,
    className: 'htRight',
    numericFormat: {
    pattern: '$0,0.00',
    culture: 'en-US',
    },
    },
    ],
    licenseKey: 'non-commercial-and-evaluation',
    };
    const hot = new Handsontable(container, hotOptions);

    Key configuration:

    • type: 'numbro' - uses the custom cell type on Quantity and Cost columns
    • numericFormat.pattern - the Numbro format string (e.g., '$0,0.00' for currency, '0,0' for integers)
    • numericFormat.culture - the locale for formatting (e.g., 'en-US', 'de-DE')
    • headerClassName: 'htLeft' - left-aligns all column headers

How It Works - Complete Flow

  1. Initial Render: Cell displays the raw number formatted by Numbro (e.g., 350000 becomes $350,000.00)
  2. User clicks cell: The built-in numeric editor opens for editing
  3. User enters number: Input is validated against the numeric validator
  4. Validation: Non-numeric values are rejected; valid numbers are accepted
  5. Save: The value is stored as a raw number and re-rendered with Numbro formatting