docs/content/guides/upgrade-and-migration/migrating-from-16.2-to-17.0/migrating-from-16.2-to-17.0.md
Migrate from Handsontable 16.2 to Handsontable 17.0, released on March 09, 2025.
More information about this release can be found in the 17.0.0 release blog post.
For a detailed list of changes in this release, see the Changelog.
[[toc]]
Starting from version 17.0.0, Handsontable 17.0 completely removes the legacy stylesheet (dist/handsontable.full.min.css). If you're upgrading from an earlier version and still using the legacy styles, you must migrate to a theme.
::: tip Using the main theme without modifications
If you want to use the main theme without any modifications, you don't need to configure anything. Handsontable will automatically use the main theme with default settings. However, if you want to retain the legacy look and feel, migrate to the Classic theme as described below.
:::
dist/handsontable.full.min.css) is no longer availableht-theme-classic) provides the same visual appearance as the legacy stylesIf you were using the legacy styles, migrate to the Classic theme using one of the two options below.
The Theme API allows you to register and configure themes programmatically with runtime features like density modes and color schemes.
<ol class="sl-steps"> <li>Update your CSS imports
- import 'handsontable/dist/handsontable.full.min.css';
Import and register the Classic theme
::: only-for javascript
import Handsontable from 'handsontable';
import { classicTheme } from 'handsontable/themes';
const hot = new Handsontable(container, {
theme: classicTheme,
// ... other options
});
:::
::: only-for react
import { HotTable } from '@handsontable/react-wrapper';
import { classicTheme } from 'handsontable/themes';
function App() {
return (
<HotTable
theme={classicTheme}
// ... other options
/>
);
}
:::
::: only-for angular
import { classicTheme } from 'handsontable/themes';
@Component({
template: `<hot-table [settings]="hotSettings"></hot-table>`
})
export class AppComponent {
hotSettings = {
theme: classicTheme,
// ... other options
};
}
:::
</li> </ol>Alternatively, you can use CSS files and pass the theme name as a string to the theme option.
Update your CSS imports
- @import 'handsontable/dist/handsontable.full.min.css';
+ @import 'handsontable/styles/ht-theme-classic.min.css';
Or if you're using JavaScript imports:
- import 'handsontable/dist/handsontable.full.min.css';
+ import 'handsontable/styles/ht-theme-classic.min.css';
::: only-for angular
Or in angular.json:
{
// ...
"styles": [
"src/styles.css",
"node_modules/handsontable/styles/ht-theme-classic.min.css"
],
// ...
}
:::
</li> <li>Set the theme in Handsontable configuration
::: only-for javascript
const hot = new Handsontable(container, {
theme: 'ht-theme-classic',
// ... other options
});
:::
::: only-for react
<HotTable
theme="ht-theme-classic"
// ... other options
/>
:::
::: only-for angular
<hot-table [settings]="{
theme: 'ht-theme-classic'
}">
</hot-table>
:::
</li> </ol>The Classic theme provides the same visual appearance as the legacy style, but with significant improvements:
ht-theme-classic-dark and ht-theme-classic-dark-auto)If you're currently using CSS-based themes (loading theme CSS files and passing theme name as a string), migrating to the Theme API provides better runtime control and customization options.
The Theme API provides a programmatic way to configure themes with runtime features that CSS-based themes don't support:
params() method::: only-for javascript
import 'handsontable/styles/handsontable.min.css';
import 'handsontable/styles/ht-theme-main.min.css';
const hot = new Handsontable(container, {
theme: 'ht-theme-main',
// ... other options
});
:::
::: only-for react
import 'handsontable/styles/handsontable.min.css';
import 'handsontable/styles/ht-theme-main.min.css';
function App() {
return (
<HotTable
theme="ht-theme-main"
// ... other options
/>
);
}
:::
::: only-for angular
// In angular.json:
// "styles": [
// "node_modules/handsontable/styles/handsontable.min.css",
// "node_modules/handsontable/styles/ht-theme-main.min.css"
// ]
@Component({
template: `<hot-table [settings]="hotSettings"></hot-table>`
})
export class AppComponent {
hotSettings = {
theme: 'ht-theme-main',
// ... other options
};
}
:::
::: only-for javascript
import Handsontable from 'handsontable';
import { mainTheme, registerTheme } from 'handsontable/themes';
const theme = registerTheme(mainTheme)
.setColorScheme('auto') // 'light', 'dark', or 'auto'
.setDensityType('default'); // 'compact', 'default', or 'comfortable'
const hot = new Handsontable(container, {
theme: theme,
// ... other options
});
// You can now change the theme at runtime
theme.setColorScheme('dark');
theme.setDensityType('compact');
:::
::: only-for react
import { HotTable } from '@handsontable/react-wrapper';
import { mainTheme, registerTheme } from 'handsontable/themes';
const theme = registerTheme(mainTheme)
.setColorScheme('auto')
.setDensityType('default');
function App() {
return (
<HotTable
theme={theme}
// ... other options
/>
);
}
:::
::: only-for angular
import { mainTheme, registerTheme } from 'handsontable/themes';
const theme = registerTheme(mainTheme)
.setColorScheme('auto')
.setDensityType('default');
@Component({
template: `<hot-table [settings]="hotSettings"></hot-table>`
})
export class AppComponent {
hotSettings = {
theme: theme,
// ... other options
};
}
:::
The Theme API allows you to customize theme parameters dynamically using the params() method:
import { mainTheme, registerTheme } from 'handsontable/themes';
const theme = registerTheme(mainTheme);
// Customize theme parameters
theme.params({
colors: {
primary: {
500: '#9333ea', // Custom primary color
},
},
tokens: {
fontSize: '14px',
borderColor: ['colors.palette.200', 'colors.palette.700'], // [light, dark]
},
});
The following themes are available through the Theme API:
| Theme | Import | Description |
|---|---|---|
| Main | mainTheme | Spreadsheet-like interface, perfect for batch-editing tasks |
| Horizon | horizonTheme | Cleaner look without vertical lines, ideal for data display |
| Classic | classicTheme | Retains the legacy style appearance with CSS variable support |
::: only-for angular
In Angular, you can set a global default theme using the HOT_GLOBAL_CONFIG injection token:
import { ApplicationConfig } from '@angular/core';
import { HOT_GLOBAL_CONFIG, HotGlobalConfig } from '@handsontable/angular-wrapper';
import { mainTheme, registerTheme } from 'handsontable/themes';
const theme = registerTheme(mainTheme).setColorScheme('auto');
export const appConfig: ApplicationConfig = {
providers: [{
provide: HOT_GLOBAL_CONFIG,
useValue: { theme: theme } as HotGlobalConfig
}],
};
Or use the HotGlobalConfigService to manage the theme at runtime:
import { HotGlobalConfigService } from '@handsontable/angular-wrapper';
import { horizonTheme, registerTheme } from 'handsontable/themes';
const theme = registerTheme(horizonTheme).setColorScheme('dark');
// In your component or service
HotGlobalConfigService.setConfig({ theme: theme });
:::
Handsontable 17.0 introduces native support for the Intl.NumberFormat API for numeric formatting. The numbro.js-based formatting (pattern and culture options) will be removed in next major release.
numericFormat option now accepts all properties of Intl.NumberFormatOptionslocale cell property instead of numericFormat.cultureThe numbro.js library added unnecessary bundle size and maintenance overhead. The native Intl.NumberFormat API provides the same functionality with better performance, broader browser support, and no external dependencies. This change aligns Handsontable with web standards and reduces the overall package size.
Update numericFormat Configuration
Replace pattern and culture properties with Intl.NumberFormat options.
Before:
::: only-for javascript
const hot = new Handsontable(container, {
columns: [
{
type: 'numeric',
numericFormat: {
pattern: '0,0.00 $',
culture: 'en-US'
}
}
]
});
:::
::: only-for react
<HotTable
columns={[{
type: 'numeric',
numericFormat: {
pattern: '0,0.00 $',
culture: 'en-US'
}
}]}
/>
:::
::: only-for angular
<hot-table [settings]="{
columns: [{
type: 'numeric',
numericFormat: {
pattern: '0,0.00 $',
culture: 'en-US'
}
}]
}"></hot-table>
:::
After:
::: only-for javascript
const hot = new Handsontable(container, {
columns: [
{
type: 'numeric',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2
}
}
]
});
:::
::: only-for react
<HotTable
columns={[{
type: 'numeric',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2
}
}]}
/>
:::
::: only-for angular
<hot-table [settings]="{
columns: [{
type: 'numeric',
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2
}
}]
}"></hot-table>
:::
</li> <li>Common Migration Patterns
Currency Formatting:
// Before
numericFormat: {
pattern: '0,0.00 $',
culture: 'en-US'
}
// After
locale: 'en-US',
numericFormat: {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2
}
Decimal Formatting:
// Before
numericFormat: {
pattern: '0,0.00',
culture: 'en-US'
}
// After
locale: 'en-US',
numericFormat: {
style: 'decimal',
useGrouping: true,
minimumFractionDigits: 2,
maximumFractionDigits: 2
}
Percent Formatting:
// Before
numericFormat: {
pattern: '0.00%',
culture: 'en-US'
}
// After
locale: 'en-US',
numericFormat: {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}
Using Numbro After Migration
If you need numbro.js-specific formatting features that aren't available in Intl.NumberFormat, you can create a custom cell type using the numbro library. See the Numbro cell type recipe for a complete implementation guide.
pattern or culture optionsIntl.NumberFormat optionsHandsontable 17.0 introduces native support for the Intl.DateTimeFormat API for date and time formatting. The Moment.js-style string formats (dateFormat and timeFormat as strings) used by the legacy date and time cell types will be removed in the next major release.
dateFormat and timeFormat options now accept all properties of Intl.DateTimeFormat options when using intl-date and intl-time cell typeslocale cell property instead of being implied by the format stringintl-date for dates and intl-time for times instead of date and time when migratingintl-date, store values in ISO 8601 date format (YYYY-MM-DD). For intl-time, store values in 24-hour format (HH:mm, HH:mm:ss, or HH:mm:ss.SSS)correctFormat option (auto-correction of entered date/time format for legacy date/time cells) and the datePickerConfig option (Pikaday-based date picker for the legacy date cell type) are deprecated and will be removed in the next major releaseMoment.js is in maintenance mode and the legacy date/time cell types depend on it for string-format parsing. The native Intl.DateTimeFormat API provides locale-aware formatting without external dependencies, with better performance and alignment with web standards. This change reduces bundle size and keeps date/time behavior consistent with the rest of the platform.
Update Date Columns
Replace the date cell type and string dateFormat with intl-date and an Intl.DateTimeFormat options object.
Before:
::: only-for javascript
const hot = new Handsontable(container, {
columns: [
{
type: 'date',
dateFormat: 'YYYY-MM-DD'
}
]
});
:::
::: only-for react
<HotTable
columns={[{
type: 'date',
dateFormat: 'YYYY-MM-DD'
}]}
/>
:::
::: only-for angular
<hot-table [settings]="{
columns: [{
type: 'date',
dateFormat: 'YYYY-MM-DD'
}]
}"></hot-table>
:::
After:
::: only-for javascript
const hot = new Handsontable(container, {
columns: [
{
type: 'intl-date',
locale: 'en-US',
dateFormat: {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}
}
]
});
:::
::: only-for react
<HotTable
columns={[{
type: 'intl-date',
locale: 'en-US',
dateFormat: {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}
}]}
/>
:::
::: only-for angular
<hot-table [settings]="{
columns: [{
type: 'intl-date',
locale: 'en-US',
dateFormat: {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}
}]
}"></hot-table>
:::
</li> <li>Update Time Columns
Replace the time cell type and string timeFormat with intl-time and an Intl.DateTimeFormat options object.
Before:
::: only-for javascript
const hot = new Handsontable(container, {
columns: [
{
type: 'time',
timeFormat: 'h:mm:ss a'
}
]
});
:::
::: only-for react
<HotTable
columns={[{
type: 'time',
timeFormat: 'h:mm:ss a'
}]}
/>
:::
::: only-for angular
<hot-table [settings]="{
columns: [{
type: 'time',
timeFormat: 'h:mm:ss a'
}]
}"></hot-table>
:::
After:
::: only-for javascript
const hot = new Handsontable(container, {
columns: [
{
type: 'intl-time',
locale: 'en-US',
timeFormat: {
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
hour12: true
}
}
]
});
:::
::: only-for react
<HotTable
columns={[{
type: 'intl-time',
locale: 'en-US',
timeFormat: {
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
hour12: true
}
}]}
/>
:::
::: only-for angular
<hot-table [settings]="{
columns: [{
type: 'intl-time',
locale: 'en-US',
timeFormat: {
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
hour12: true
}
}]
}"></hot-table>
:::
</li> <li>Common Migration Patterns
Short date (e.g. DD/MM/YYYY → locale short):
// Before
type: 'date',
dateFormat: 'DD/MM/YYYY'
// After
type: 'intl-date',
locale: 'en-GB',
dateFormat: { dateStyle: 'short' }
ISO date (YYYY-MM-DD):
// Before
type: 'date',
dateFormat: 'YYYY-MM-DD'
// After
type: 'intl-date',
locale: 'en-US',
dateFormat: {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}
12-hour time with seconds:
// Before
type: 'time',
timeFormat: 'h:mm:ss a'
// After
type: 'intl-time',
locale: 'en-US',
timeFormat: {
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
hour12: true
}
Time style shortcut:
// After
type: 'intl-time',
locale: 'en-US',
timeFormat: { timeStyle: 'medium' }
Custom Cell Types Using Moment.js
If you use custom cell types that rely on Moment.js for formatting or parsing (e.g. recipes like the Moment.js date or time cell type), replace Moment formatting with Intl.DateTimeFormat in your renderer and editor logic. For a full custom implementation that still uses Moment, see the Moment.js date and Moment.js time recipes; consider migrating those implementations to Intl to avoid the deprecated path.
dateFormat or timeFormat with the date or time cell types, or if you use the correctFormat or datePickerConfig optionsYYYY-MM-DD and time values in 24-hour form; intl-date/intl-time only change display, not storagedateStyle/timeStyle or component options to approximate previous Moment format outputdateFormat and timeFormat, and the correctFormat and datePickerConfig options, are deprecated and emit console warnings; the intl-date and intl-time cell types are available.intl-date and intl-time cell types will become the default date and time cell types; intl-date and intl-time will remain as aliases. The Moment.js library will be removed from dependencies. Options correctFormat and datePickerConfig will be removed.Handsontable 17.0 introduces a configurable sanitizer option for HTML content. The built-in use of the DOMPurify library is deprecated and will be removed in the next major release. After that, if you do not set a custom sanitizer, any string containing HTML will be stripped before rendering (no rich HTML). To keep sanitized HTML (e.g. with DOMPurify or another library), set the sanitizer option to your own sanitizer function.
sanitizer option (v17.0): A table-level option that accepts a function (content, source) => string. It is used whenever HTML is written to the DOM (cell values, headers, context menu labels, dialog markup, clipboard paste).DOMPurify by default but shows a deprecation warning. In the next major release, DOMPurify is removed; with no custom sanitizer, HTML in content will be stripped.'innerHTML' or 'CopyPaste.paste'), so you can apply different rules per context.Making sanitization configurable gives you control over allowlists, library choice, and integration with Trusted Types and CSP. It also allows removing DOMPurify from the bundle for applications that use a different sanitization strategy or none.
Install DOMPurify in your project and pass a function that calls it via the sanitizer option:
::: only-for javascript
import DOMPurify from 'dompurify';
import Handsontable from 'handsontable';
const hot = new Handsontable(container, {
sanitizer: (content, source) => {
if (source === 'CopyPaste.paste') {
return DOMPurify.sanitize(content, {
ADD_TAGS: ['meta'],
ADD_ATTR: ['content'],
FORCE_BODY: true,
});
}
return DOMPurify.sanitize(content);
},
// ... other options
});
:::
::: only-for react
import DOMPurify from 'dompurify';
import { HotTable } from '@handsontable/react-wrapper';
function App() {
return (
<HotTable
sanitizer={(content, source) => {
if (source === 'CopyPaste.paste') {
return DOMPurify.sanitize(content, {
ADD_TAGS: ['meta'],
ADD_ATTR: ['content'],
FORCE_BODY: true,
});
}
return DOMPurify.sanitize(content);
}}
// ... other options
/>
);
}
:::
::: only-for angular
import DOMPurify from 'dompurify';
@Component({
template: `<hot-table [settings]="hotSettings"></hot-table>`
})
export class AppComponent {
hotSettings = {
sanitizer: (content: string, source: string) {
if (source === 'CopyPaste.paste') {
return DOMPurify.sanitize(content, {
ADD_TAGS: ['meta'],
ADD_ATTR: ['content'],
FORCE_BODY: true,
});
}
return DOMPurify.sanitize(content);
},
// ... other options
};
}
:::
If you do not want rich HTML, use a sanitizer that escapes or strips tags:
sanitizer: (content, source) => {
const tpl = document.createElement('template');
tpl.innerHTML = content;
const text = tpl.content.textContent ?? '';
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
If you enforce Trusted Types, wrap your sanitizer in a policy and return its createHTML result. Add the policy name to your CSP trusted-types directive (e.g. trusted-types default handsontable):
const policy = window.trustedTypes?.createPolicy('handsontable', {
createHTML: (input) => DOMPurify.sanitize(input),
});
sanitizer: (content, source) =>
policy ? policy.createHTML(content) : DOMPurify.sanitize(content),
DOMPurify) without setting sanitizer, a deprecation warning is shown pointing to the sanitizer option.DOMPurify is removed from dependencies. Without a custom sanitizer, HTML in cell content, headers, menus, and paste will be stripped before rendering.sanitizer option is available; default remains DOMPurify with a deprecation warning.DOMPurify is removed. If no sanitizer is set, HTML content is stripped.sanitizer optioncore-js dependency removedStarting in version 17.0, Handsontable no longer depends on or bundles core-js. The library relied on it in the past for polyfills (e.g. ECMAScript 5/6 features, Promises, Symbols, collections). Handsontable removes that dependency to reduce bundle size and to avoid forcing a specific polyfill set on applications that target modern environments only.
core-js package is no longer a dependency of Handsontablecore-js added significant size to the bundle and was unnecessary for applications that only support modern browsers and Node versions. Dropping it lets the library stay smaller and lets each application choose its own polyfill strategy (or none) based on its own target environment.
If your application or build previously relied on Handsontable (or its build tooling) to pull in core-js, and you still need to support older environments that lack certain APIs:
core-js (or another polyfill library) in your own project and load it before Handsontable, e.g. in your entry file or polyfills bundle.If you only target modern browsers and runtimes, no action is needed.
core-js is removed from Handsontable dependencies and build outputThe Formulas plugin uses HyperFormula as its calculation engine. Currently, HyperFormula is bundled with Handsontable. In version 18.0, it will be removed from package.json.
npm install hyperformula).licenseKey: 'internal-use-in-handsontable'.See the Formula calculation guide for configuration details.
| Change | Action Required |
|---|---|
| Legacy stylesheet removed | Migrate to Classic theme or another built-in theme |
handsontable.full.min.css no longer available | Use Theme API or import a theme CSS file (e.g. ht-theme-classic.min.css) |
| CSS-based themes (optional migration) | Consider migrating to Theme API for runtime features |
core-js dependency removed | Add core-js or other polyfills in your app if you support older environments |
| Built-in HyperFormula (deprecation) | In 18.0, import HyperFormula yourself and pass it to the Formulas plugin with licenseKey: 'internal-use-in-handsontable' |
Your application now runs on Handsontable 17.0.