docs/content/guides/upgrade-and-migration/migrating-from-15.3-to-16.0/migrating-from-15.3-to-16.0.md
Migrate from Handsontable 15.3 to Handsontable 16.0, released on July 9, 2025.
More information about this release can be found in the 16.0.0 release blog post.
For a detailed list of changes in this release, see the Changelog.
[[toc]]
In Handsontable 16.0, the table mounts to the DOM differently. Previously, the container <div> you provided became the root element of the table. Now, that container acts as a mounting point, and Handsontable creates and injects its own root element inside it.
Here's a side-by-side comparison of the old and new DOM structures:
Old DOM structure:
<starlight-file-tree class="not-content"> <ul> <li class="directory"><details open><summary><span class="tree-entry"><span>body</span></span></summary> <ul> <li class="directory"><details open><summary><span class="tree-entry"><span>#example.ht-wrapper.handsontable</span> <span class="comment">Root Container/Element</span></span></summary> <ul> <li class="file"><span class="tree-entry"><span>.htFocusCatcher</span> <span class="comment">Focus Catcher (top)</span></span></li> <li class="file"><span class="tree-entry"><span>Data grid content</span></span></li> <li class="file"><span class="tree-entry"><span>.htFocusCatcher</span> <span class="comment">Focus Catcher (down)</span></span></li> </ul></details></li> <li class="file"><span class="tree-entry"><span>.hot-display-license-info</span> <span class="comment">License key notification bar</span></span></li> <li class="file"><span class="tree-entry"><span>Context menus, dropdowns, pop-ups, sidebars</span> <span class="comment">absolutely positioned elements</span></span></li> </ul></details></li> </ul> </starlight-file-tree>New DOM structure:
<starlight-file-tree class="not-content"> <ul> <li class="directory"><details open><summary><span class="tree-entry"><span>body</span></span></summary> <ul> <li class="directory"><details open><summary><span class="tree-entry"><span>#example</span> <span class="comment">Root Wrapper</span></span></summary> <ul> <li class="directory"><details open><summary><span class="tree-entry"><span>.ht-root-wrapper</span> <span class="comment">Root Element</span></span></summary> <ul> <li class="file"><span class="tree-entry"><span>.htFocusCatcher</span> <span class="comment">Focus Catcher (top)</span></span></li> <li class="directory"><details open><summary><span class="tree-entry"><span>.ht-wrapper.handsontable</span> <span class="comment">Root Container</span></span></summary> <ul> <li class="file"><span class="tree-entry"><span>Data grid content</span></span></li> </ul></details></li> <li class="file"><span class="tree-entry"><span>.htFocusCatcher</span> <span class="comment">Focus Catcher (down)</span></span></li> <li class="file"><span class="tree-entry"><span>.hot-display-license-info</span> <span class="comment">License key notification bar</span></span></li> </ul></details></li> </ul></details></li> <li class="directory"><details open><summary><span class="tree-entry"><span>.ht-portal</span> <span class="comment">Portal Element</span></span></summary> <ul> <li class="file"><span class="tree-entry"><span>Context menus, dropdowns, pop-ups, sidebars</span> <span class="comment">absolutely positioned elements</span></span></li> </ul></details></li> </ul></details></li> </ul> </starlight-file-tree>In Handsontable 16.0, Handsontable improves the CSS variables system to adjust theme colors, variable order, and customization options. Here are the key changes:
These new variables allow for easier customization:
--ht-letter-spacing: Controls letter spacing for improved readability and visual appearance.--ht-radio-*: Enables more accurate styling of radio inputs.--ht-cell-read-only-background-color: Allows better customization of read-only cell backgrounds.--ht-checkbox-indeterminate: Lets you style the indeterminate state of checkboxes.A few variables are renamed for more consistent naming:
| Old variable name | New variable name |
|---|---|
--ht-icon-active-button-border-color | --ht-icon-button-active-border-color |
--ht-icon-active-button-background-color | --ht-icon-button-active-background-color |
--ht-icon-active-button-icon-color | --ht-icon-button-active-icon-color |
--ht-icon-active-button-hover-border-color | --ht-icon-button-active-hover-border-color |
--ht-icon-active-button-hover-background-color | --ht-icon-button-active-hover-background-color |
--ht-icon-active-button-hover-icon-color | --ht-icon-button-active-hover-icon-color |
If you were using custom CSS variables in version 15.3, you'll need to:
In version 16.0, Handsontable updates how custom borders are positioned to improve accuracy and consistency. This change affects the visual positioning of borders, particularly for cells with custom borders.
It's very unlikely, but if your application relies on specific border positioning or you've implemented custom styling based on border positions, you may need to update your styles.
The visual appearance of borders in version 16.0 will be slightly different compared to version 15.3.
No code changes are required - the improvements are handled automatically by the new version.
Handsontable 16.0 introduces a completely new Angular wrapper for Handsontable. This wrapper is designed to provide better integration with modern Angular applications and improved developer experience. If you use Angular 16 or higher, we recommend migrating to the new wrapper.
ViewChild.Replace the old Angular wrapper package with the new one:
npm uninstall @handsontable/angular
npm install @handsontable/angular-wrapper
Move all configuration options to a GridSettings object in your component.
Old wrapper component:
@Component({
selector: 'app-root',
template: `
<hot-table
[data]="data"
[colHeaders]="true"
[licenseKey]="'non-commercial-and-evaluation'">
<hot-column data="id" [readOnly]="true" title="ID"></hot-column>
<hot-column data="name" title="Full name"></hot-column>
</hot-table>
`
})
export class AppComponent {
data = //...
}
New wrapper component:
import { GridSettings, HotTableModule } from '@handsontable/angular-wrapper';
@Component({
standalone: true,
imports: [HotTableModule],
template: `<hot-table [data]="data" [settings]="gridSettings" />`
})
export class AppComponent {
data = //...;
gridSettings: GridSettings = {
colHeaders: true,
licenseKey: 'non-commercial-and-evaluation',
columns: [
{ data: 'id', readOnly: true, title: 'ID' },
{ data: 'name', title: 'Full name' },
]
};
}
The way you reference and interact with the Handsontable instance has changed.
Old wrapper instance reference:
export class AppComponent {
private hotRegisterer = new HotTableRegisterer();
id = 'hotInstance';
swapHotData() {
this.hotRegisterer.getInstance(this.id).loadData([['new', 'data']]);
}
}
New wrapper instance reference:
import { HotTableComponent } from '@handsontable/angular-wrapper';
export class AppComponent {
@ViewChild(HotTableComponent, { static: false })
hotTable!: HotTableComponent;
swapHotData() {
this.hotTable.hotInstance!.loadData([['new', 'data']]);
}
}
The new wrapper provides better global configuration management.
Old wrapper global configuration:
// Configuration was typically done per component
export class AppComponent {
hotSettings = {
licenseKey: 'non-commercial-and-evaluation',
};
}
New wrapper global configuration using ApplicationConfig:
import { ApplicationConfig } from '@angular/core';
import { HOT_GLOBAL_CONFIG, HotGlobalConfig, NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
const globalHotConfig: HotGlobalConfig = {
license: NON_COMMERCIAL_LICENSE,
language: 'en',
themeName: 'ht-theme-main',
};
export const appConfig: ApplicationConfig = {
providers: [
{ provide: HOT_GLOBAL_CONFIG, useValue: globalHotConfig },
],
};
New wrapper global configuration using service:
import { HotGlobalConfigService, NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
export class AppComponent {
constructor(private hotConfig: HotGlobalConfigService) {
this.hotConfig.setConfig({
themeName: 'ht-theme-main',
});
}
}
The new wrapper introduces component-based editors alongside the traditional class-based approach.
Old wrapper custom editor:
import { TextEditor } from 'handsontable/editors/textEditor';
export class CustomEditor extends TextEditor {
override createElements() {
super.createElements();
this.TEXTAREA = document.createElement('input');
this.TEXTAREA.setAttribute('placeholder', 'Custom placeholder');
this.TEXTAREA.setAttribute('data-hot-input', 'true');
this.textareaStyle = this.TEXTAREA.style;
this.TEXTAREA_PARENT.innerText = '';
this.TEXTAREA_PARENT.appendChild(this.TEXTAREA);
}
}
// Usage in settings
hotSettings = {
columns: [{ editor: CustomEditor }]
};
New wrapper component-based editor:
import { Component, ViewChild, ElementRef, ChangeDetectionStrategy } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HotCellEditorComponent } from '@handsontable/angular-wrapper';
@Component({
selector: 'app-custom-editor',
imports: [FormsModule],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div style="width: 100%; overflow: hidden">
<input
#inputElement
type="text"
[value]="getValue()"
(keydown)="onKeyDown($event)"
style="width: 100%; box-sizing: border-box"
/>
</div>
`,
})
export class CustomEditorComponent extends HotCellEditorComponent<string> {
@ViewChild('inputElement') inputElement!: ElementRef;
onKeyDown(event: KeyboardEvent): void {
const target = event.target as HTMLInputElement;
this.setValue(target.value);
}
onFocus(): void {
this.inputElement.nativeElement.select();
}
}
// Usage in settings
gridSettings: GridSettings = {
columns: [{ editor: CustomEditorComponent }]
};
The new wrapper supports component-based renderers in addition to function-based renderers.
Old wrapper custom renderer:
export class AppComponent {
hotSettings = {
columns: [{
renderer(instance, td, row, col, prop, value, cellProperties) {
const img = document.createElement('img');
img.src = value;
td.innerText = '';
td.appendChild(img);
return td;
}
}]
};
}
New wrapper component-based renderer:
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { HotCellRendererComponent } from '@handsontable/angular-wrapper';
@Component({
selector: 'app-custom-renderer',
template: `
<div class="container" [style.backgroundColor]="value">
{{ value }}
</div>
`,
styles: [`
.container {
height: 100%;
width: 100%;
}
:host {
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
padding: 0;
}
`],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomRendererComponent extends HotCellRendererComponent<string> {}
// Usage in settings
gridSettings: GridSettings = {
columns: [{ renderer: CustomRendererComponent }]
};
Ensure you're importing the correct CSS files for themes.
CSS imports (same for both wrappers):
@import 'handsontable/styles/ht-theme-main.min.css';
Or in angular.json:
{
"styles": [
"src/styles.scss",
"node_modules/handsontable/styles/handsontable.min.css",
"node_modules/handsontable/styles/ht-theme-main.min.css"
]
}
Issue: "Cannot find module '@handsontable/angular'"
@handsontable/angular-wrapperIssue: "hot-column is not recognized"
<hot-column>. Move column configuration to the columns array in your settings object.Issue: "HotTableRegisterer is not defined"
@ViewChild(HotTableComponent) and access the hotInstance property instead.Issue: "Custom renderer not working"
HotCellRendererComponent.Issue: "Custom editor not working"
HotCellEditorComponent.This migration guide covers the major changes between the old and new Angular wrappers. The new wrapper provides better integration with modern Angular patterns, improved type safety, and a more maintainable codebase.
pnpm as the repository package managerStarting on July 1st, 2025, Handsontable switches to pnpm as the repository's main package manager.
As the number of packages in the repository grew, so did the number of dependencies. This made it difficult to manage dependencies and install them in a consistent way. To address this, the project switches to pnpm as the main package manager.
Unless you're not creating custom builds of Handsontable or any of the wrappers, this change will not affect you.
If you are, however, you'll need to utilize pnpm to install the main repository dependencies.
Note: The examples and docs packages are still managed with npm, and are not a part of the main pnpm workspace.
Install pnpm with a version corresponding to the one defined in the packageManager field of the root's package.json.
If you worked on your clone of the repository before, you'll need to remove the node_modules directory, package-lock.json files etc.
You can do this by running npm run clean:node_modules -- --keep-lockfiles.
Run pnpm install to install the dependencies.
All the npm commands are still available, so you can build the packages as you did before, for example, by running npm run build.
You can always find more information on the custom build process in the Custom builds documentation page.
Your application now runs on Handsontable 16.0.