Skip to content

Display, format, sort, and filter numbers correctly by using the numeric cell type.

Overview

The default cell type in Handsontable is text. The data of a text cell is processed as a string type that corresponds to the value of the text editor’s internal <textarea> element. However, there are many cases where you need cell values to be treated as a number type. The numeric cell type allows you to format displayed numbers nicely and sort them correctly.

Numeric cell type demo

In the following demo, multiple columns use the numeric cell type with different formatting styles:

  • Year: Basic numeric formatting
  • Price (USD) and Price (EUR): Currency formatting
  • Distance: Unit formatting (kilometers) with grouping
  • Fuel: Unit formatting (liters) with decimal precision
  • Discount: Percentage formatting
  • Quantity: Decimal formatting with thousands separators

Use the locale selector above the table to see how different locales affect number formatting.

TypeScript
/* file: app.component.ts */
import { Component, ViewChild, ViewEncapsulation, HostListener, ElementRef } from '@angular/core';
import { GridSettings, HotTableComponent } from '@handsontable/angular-wrapper';
interface CarData {
car: string;
year: number;
price_usd: number;
price_eur: number;
distance_km: number;
fuel_liters: number;
discount_percent: number;
quantity: number;
}
@Component({
selector: 'example1-numeric-cell-type',
standalone: false,
template: `
<div class="example-controls-container">
<div class="controls">
<div class="theme-dropdown" #dropdownRef>
<button
class="theme-dropdown-trigger"
type="button"
aria-haspopup="listbox"
[attr.aria-expanded]="isOpen"
(click)="toggleDropdown()"
>
<span>{{ selectedLabel }}</span>
<svg class="theme-dropdown-chevron" aria-hidden="true" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6l6 -6"/></svg>
</button>
<ul class="theme-dropdown-menu" role="listbox" *ngIf="isOpen">
<li
*ngFor="let opt of localeOptions"
role="option"
[attr.aria-selected]="locale === opt.value"
(click)="selectLocale(opt.value)"
>
{{ opt.label }}
</li>
</ul>
</div>
</div>
</div>
<div>
<hot-table [data]="data" [settings]="gridSettings"></hot-table>
</div>
`,
encapsulation: ViewEncapsulation.None
})
export class Example1NumericCellTypeComponent {
@ViewChild(HotTableComponent, { static: false }) readonly hotTable!: HotTableComponent;
isOpen = false;
locale = 'en-US';
76 collapsed lines
readonly localeOptions = [
{ value: 'ar-AR', label: 'Arabic (Global)' },
{ value: 'cs-CZ', label: 'Czech (Czechia)' },
{ value: 'de-CH', label: 'German (Switzerland)' },
{ value: 'de-DE', label: 'German (Germany)' },
{ value: 'en-US', label: 'English (United States)' },
{ value: 'es-MX', label: 'Spanish (Mexico)' },
{ value: 'fa-IR', label: 'Persian (Iran)' },
{ value: 'fr-FR', label: 'French (France)' },
{ value: 'hr-HR', label: 'Croatian (Croatia)' },
{ value: 'it-IT', label: 'Italian (Italy)' },
{ value: 'ja-JP', label: 'Japanese (Japan)' },
{ value: 'ko-KR', label: 'Korean (Korea)' },
{ value: 'lv-LV', label: 'Latvian (Latvia)' },
{ value: 'nb-NO', label: 'Norwegian Bokmal (Norway)' },
{ value: 'nl-NL', label: 'Dutch (Netherlands)' },
{ value: 'pl-PL', label: 'Polish (Poland)' },
{ value: 'pt-BR', label: 'Portuguese (Brazil)' },
{ value: 'ru-RU', label: 'Russian (Russia)' },
{ value: 'sr-SP', label: 'Serbian Latin (Serbia)' },
{ value: 'zh-CN', label: 'Chinese (Simplified, China)' },
{ value: 'zh-TW', label: 'Chinese (Traditional, Taiwan)' },
];
readonly data: CarData[] = [
{
car: 'Mercedes A 160',
year: 2017,
price_usd: 7000,
price_eur: 7000,
distance_km: 125000,
fuel_liters: 45.5,
discount_percent: 0.15,
quantity: 1250
},
{
car: 'Citroen C4 Coupe',
year: 2018,
price_usd: 8330,
price_eur: 8330,
distance_km: 98000,
fuel_liters: 52.3,
discount_percent: 0.08,
quantity: 2100
},
{
car: 'Audi A4 Avant',
year: 2019,
price_usd: 33900,
price_eur: 33900,
distance_km: 45000,
fuel_liters: 60.0,
discount_percent: 0.05,
quantity: 850
},
{
car: 'Opel Astra',
year: 2020,
price_usd: 5000,
price_eur: 5000,
distance_km: 156000,
fuel_liters: 48.7,
discount_percent: 0.12,
quantity: 3200
},
{
car: 'BMW 320i Coupe',
year: 2021,
price_usd: 30500,
price_eur: 30500,
distance_km: 32000,
fuel_liters: 55.2,
discount_percent: 0.03,
quantity: 1500
},
];
readonly gridSettings: GridSettings = {
colHeaders: ['Car', 'Year', 'Price (USD)', 'Price (EUR)', 'Distance', 'Fuel', 'Discount', 'Quantity'],
locale: this.locale,
columnSorting: true,
filters: true,
dropdownMenu: true,
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
65 collapsed lines
columns: [
{
data: 'car',
type: 'text',
},
{
data: 'year',
type: 'numeric',
},
{
data: 'price_usd',
type: 'numeric',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
},
},
{
data: 'price_eur',
type: 'numeric',
numericFormat: {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 2,
},
},
{
data: 'distance_km',
type: 'numeric',
numericFormat: {
style: 'unit',
unit: 'kilometer',
useGrouping: true,
},
},
{
data: 'fuel_liters',
type: 'numeric',
numericFormat: {
style: 'unit',
unit: 'liter',
minimumFractionDigits: 1,
maximumFractionDigits: 1,
},
},
{
data: 'discount_percent',
type: 'numeric',
numericFormat: {
style: 'percent',
minimumFractionDigits: 0,
maximumFractionDigits: 2,
},
},
{
data: 'quantity',
type: 'numeric',
numericFormat: {
style: 'decimal',
useGrouping: true,
minimumFractionDigits: 0,
},
},
]
};
get selectedLabel(): string {
return this.localeOptions.find((o) => o.value === this.locale)?.label || '';
}
constructor(private elementRef: ElementRef) {}
toggleDropdown(): void {
this.isOpen = !this.isOpen;
}
selectLocale(value: string): void {
this.locale = value;
this.isOpen = false;
this.hotTable?.hotInstance?.updateSettings({ locale: this.locale });
}
@HostListener('document:click', ['$event'])
onDocumentClick(event: MouseEvent): void {
if (!this.elementRef.nativeElement.querySelector('.theme-dropdown')?.contains(event.target)) {
this.isOpen = false;
}
}
@HostListener('document:keydown', ['$event'])
onKeydown(event: KeyboardEvent): void {
if (event.key === 'Escape') {
this.isOpen = false;
}
}
}
/* 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 { Example1NumericCellTypeComponent } 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: [ Example1NumericCellTypeComponent ],
providers: [...appConfig.providers],
bootstrap: [ Example1NumericCellTypeComponent ]
})
export class AppModule { }
/* end-file */
HTML
<div>
<example1-numeric-cell-type></example1-numeric-cell-type>
</div>

Use the numeric cell type

To use the numeric cell type, set the type option to 'numeric':

// set the numeric cell type for each cell of the entire grid
settings1 = {
type: "numeric",
};
// set the numeric cell type for each cell of a single column
settings2 = {
columns: [
{
type: "numeric",
},
],
};
// set the numeric cell type for a single cell
settings3 = {
cell: [
{
row: 0,
col: 0,
type: "numeric",
},
],
};

Mind that Handsontable doesn’t parse strings to numbers. In your data source, make sure to store numeric cell values as numbers, not as strings.

All positive and negative integers whose magnitude is no greater than 253 (+/- 9007199254740991) are representable in the Number type, i.e., as a safe integer. Any calculations that are performed on bigger numbers won’t be calculated precisely, due to JavaScript’s limitations.

Format numbers

To format the look of numeric values in cell renderers, use the numericFormat option.

Since Handsontable 17.0, the numericFormat option supports the native Intl.NumberFormat API, which provides better performance and broader browser support without external dependencies.

The numericFormat option accepts all properties of Intl.NumberFormatOptions. The locale is controlled separately via the locale option.

settings = {
columns: [
{
type: 'numeric',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2
}
},
{
type: 'numeric',
locale: 'de-DE',
numericFormat: {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 2
}
}
]
};

Common formatting styles:

  • Currency: Use style: 'currency' with a currency property (e.g., 'USD', 'EUR', 'PLN')
  • Decimal: Use style: 'decimal' with useGrouping: true for thousands separators
  • Percent: Use style: 'percent' for percentage formatting
  • Unit: Use style: 'unit' with a unit property (e.g., 'kilometer', 'liter')

Available options:

Style options:

PropertyPossible valuesDescription
style'decimal' (default), 'currency', 'percent', 'unit'The formatting style to use
currencyISO 4217 currency codes (e.g., 'USD', 'EUR', 'PLN')Required when style is 'currency'
currencyDisplay'symbol' (default), 'narrowSymbol', 'code', 'name'How to display the currency
currencySign'standard' (default), 'accounting'Use parentheses for negative values in accounting format
unitUnit identifiers (e.g., 'kilometer', 'liter')Required when style is 'unit'
unitDisplay'short' (default), 'narrow', 'long'How to display the unit

Notation options:

PropertyPossible valuesDescription
notation'standard' (default), 'scientific', 'engineering', 'compact'The formatting notation
compactDisplay'short' (default), 'long'Display style for compact notation (e.g., 1.5M vs 1.5 million)

Sign and grouping options:

PropertyPossible valuesDescription
signDisplay'auto' (default), 'never', 'always', 'exceptZero', 'negative'When to display the sign
useGroupingtrue, false (default), 'always', 'auto', 'min2'Whether to use grouping separators (e.g., 1,000)

Digit options:

PropertyPossible valuesDescription
minimumIntegerDigits1 to 21Minimum number of integer digits (pads with zeros)
minimumFractionDigits0 to 100Minimum number of fraction digits
maximumFractionDigits0 to 100Maximum number of fraction digits
minimumSignificantDigits1 to 21Minimum number of significant digits
maximumSignificantDigits1 to 21Maximum number of significant digits

Rounding options:

PropertyPossible valuesDescription
roundingMode'halfExpand' (default), 'ceil', 'floor', 'expand', 'trunc', 'halfCeil', 'halfFloor', 'halfTrunc', 'halfEven'Rounding algorithm
roundingPriority'auto' (default), 'morePrecision', 'lessPrecision'Priority between fraction and significant digits
roundingIncrement1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000Increment for rounding (e.g., nickel rounding)
trailingZeroDisplay'auto' (default), 'stripIfInteger'Whether to strip trailing zeros for integers

Locale options:

PropertyPossible valuesDescription
localeMatcher'best fit' (default), 'lookup'Locale matching algorithm
numberingSystem'latn', 'arab', 'hans', 'deva', 'thai', etc.Numbering system to use

For a complete reference, see the numericFormat API documentation or MDN: Intl.NumberFormat.

Using Numbro.js format options (deprecated)

The following demo uses the deprecated numbro.js format options. These options are still supported but will be removed in version 18.0.

In the following demo, columns Price in Japan and Price in Turkey use two different numericFormat configurations.

TypeScript
/* file: app.component.ts */
import { Component } from '@angular/core';
import { GridSettings } from '@handsontable/angular-wrapper';
@Component({
selector: 'example3-numeric-cell-type',
standalone: false,
template: ` <div>
<hot-table [data]="data" [settings]="gridSettings"></hot-table>
</div>`,
})
export class Example3NumericCellTypeComponent {
readonly data = [
{
productName: 'Product A',
JP_price: 1450.32,
TR_price: 202.14,
},
{
productName: 'Product B',
JP_price: 2430.22,
TR_price: 338.86,
},
{
productName: 'Product C',
JP_price: 3120.1,
TR_price: 435.2,
},
];
readonly gridSettings: GridSettings = {
autoRowSize: false,
autoColumnSize: false,
columnSorting: true,
colHeaders: ['Product name', 'Price in Japan', 'Price in Turkey'],
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
columns: [
{ data: 'productName', type: 'text', width: '150' },
{
data: 'JP_price',
type: 'numeric',
locale: 'ja-JP',
numericFormat: { style: 'decimal', useGrouping: true, minimumFractionDigits: 2, maximumFractionDigits: 2 },
width: '150',
},
{
data: 'TR_price',
type: 'numeric',
locale: 'tr-TR',
numericFormat: { style: 'decimal', useGrouping: true, minimumFractionDigits: 2, maximumFractionDigits: 2 },
width: '150',
},
]
};
}
/* 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 { Example3NumericCellTypeComponent } 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: [ Example3NumericCellTypeComponent ],
providers: [...appConfig.providers],
bootstrap: [ Example3NumericCellTypeComponent ]
})
export class AppModule { }
/* end-file */
HTML
<div>
<example3-numeric-cell-type></example3-numeric-cell-type>
</div>

Deprecated options:

OptionDescriptionReplacement
patternNumbro.js format pattern (e.g., '0,0.00 $')Use Intl.NumberFormat options (see above)
cultureNumbro.js locale identifier (e.g., 'en-US')Use the locale option

Migration example:

// Before (deprecated)
numericFormat: {
pattern: '0,0.00 $',
culture: 'en-US'
}
// After (recommended)
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2
}

For detailed migration instructions and more examples, see the migration guide.

Editor behavior

Mind that the numericFormat option doesn’t change the way numbers are presented or parsed by the cell editor. When you edit a numeric cell:

  • Regardless of the numericFormat configuration, the number that’s being edited displays its decimal separator as a period (.), and has no thousands separator or currency symbol.
    For example, during editing $7,000.02, the number displays as 7000.02.
  • You can enter a decimal separator either with a period (.), or with a comma (,).
  • You can’t enter a thousands separator. After you finish editing the cell, the thousands separator is added automatically, based on your numericFormat configuration.

Related guides

Configuration options

Core methods

Hooks