21 KiB
name, description
| name | description |
|---|---|
| handsontable | Use this skill whenever the user asks about Handsontable — a JavaScript data grid component with spreadsheet-like UX. Triggers include: mentions of "handsontable", "HotTable", "data grid", "@handsontable/react-wrapper", spreadsheet component in React/Angular/Vue, HyperFormula formulas inside a grid, cell types (dropdown, checkbox, date, numeric) in a grid context, or questions about Handsontable theming, plugins, hooks, configuration options, or column/row features. Also trigger when the user wants to add an editable spreadsheet-like table to a web app, or asks about copy/paste, sorting, filtering, frozen rows/columns, merged cells, or context menus in a grid. This skill covers Handsontable used with any framework (React, Angular, Vue 3, vanilla JS) and HyperFormula as the Formulas plugin engine inside Handsontable. Do NOT use this skill for headless/standalone HyperFormula usage without Handsontable — that is covered by the separate "hyperformula" skill. |
Handsontable
Handsontable is a JavaScript data grid component (not a full spreadsheet application) that brings spreadsheet-like UX to web apps: cell editing, copy/paste, sorting, filtering, formulas, keyboard navigation, context menus, merged cells, frozen rows/columns, conditional formatting, data validation, pagination, and 400+ built-in formulas via HyperFormula.
- Latest version: 17.1.0 (May 2026)
- Frameworks: Vanilla JS/TS, React (
@handsontable/react-wrapper), Angular (@handsontable/angular-wrapper), Vue 3 (@handsontable/vue3) - React wrapper requires: React 18+
- License: Dual — free for non-commercial use (
licenseKey: 'non-commercial-and-evaluation'), paid for commercial. Per-developer annual license, offline validation (no server connection). Tiers: Hobby (free, non-commercial), Trial (free 45 days), Standard (from $999/yr), Priority (from $1,299/yr), Enterprise (custom). See Pricing.
Always check references/docs-map.md (in this skill folder) for the full organized link directory
when you need to point the user to specific documentation or need to look up more info.
What this skill does NOT cover
- Standalone HyperFormula (headless formula engine without a grid UI) — use the "hyperformula" skill instead.
- Other data grid libraries (AG Grid, TanStack Table, etc.).
- Full spreadsheet applications — Handsontable is a component you embed, not a standalone app like Google Sheets.
React Quick Start (Recommended)
Install
npm install handsontable @handsontable/react-wrapper
Minimal working example
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import 'handsontable/styles/handsontable.min.css';
import 'handsontable/styles/ht-theme-main.min.css';
registerAllModules();
const MyGrid = () => (
<HotTable
themeName="ht-theme-main"
data={[
['', 'Tesla', 'Volvo', 'Toyota', 'Ford'],
['2019', 10, 11, 12, 13],
['2020', 20, 11, 14, 13],
['2021', 30, 15, 12, 13],
]}
colHeaders={true}
rowHeaders={true}
height="auto"
autoWrapRow={true}
autoWrapCol={true}
licenseKey="non-commercial-and-evaluation"
/>
);
export default MyGrid;
Key points:
registerAllModules()registers every built-in plugin. To reduce bundle size, import only what you need — see Modules guide.heightis required (or the grid won't render). Use"auto", a pixel number, or a CSS string.- All Handsontable configuration options are passed as props on
<HotTable>. - Full installation guide: https://handsontable.com/docs/react-data-grid/installation/
Vanilla JS Quick Start
Install
npm install handsontable
CDN (jsDelivr)
<script src="https://cdn.jsdelivr.net/npm/handsontable/dist/handsontable.full.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/handsontable/styles/handsontable.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/handsontable/styles/ht-theme-main.min.css" />
To pin a specific version, add @17.1 after handsontable in the URL (e.g.,
handsontable@17.1/dist/handsontable.full.min.js).
Minimal working example
import Handsontable from 'handsontable';
import 'handsontable/styles/handsontable.min.css';
import 'handsontable/styles/ht-theme-main.min.css';
const container = document.getElementById('grid');
new Handsontable(container, {
data: [
['', 'Tesla', 'Volvo', 'Toyota', 'Ford'],
['2019', 10, 11, 12, 13],
['2020', 20, 11, 14, 13],
['2021', 30, 15, 12, 13],
],
colHeaders: true,
rowHeaders: true,
height: 'auto',
themeName: 'ht-theme-main',
autoWrapRow: true,
autoWrapCol: true,
licenseKey: 'non-commercial-and-evaluation',
});
Full JS docs: https://handsontable.com/docs/javascript-data-grid/installation/
Other Frameworks
Angular (v17–19)
npm install handsontable @handsontable/angular-wrapper
The Angular wrapper was modernized in Handsontable v17.1 to align with Angular 17–19, simplifying setup and reducing dependencies. Earlier Angular versions are no longer the target — upgrade Angular first if you're below v17.
Docs: https://handsontable.com/docs/angular-data-grid/installation/
Vue 3
npm install handsontable @handsontable/vue3
Docs: https://handsontable.com/docs/react-data-grid/vue3-installation/
All framework wrappers share the same version number as the core library and expose the same
configuration options. The React examples in this skill translate directly — just use the
framework's component syntax. Note: Vue 2 (@handsontable/vue) is deprecated — use Vue 3.
Theming: Light / Dark / Auto
Handsontable v15+ includes a built-in theme system. Three themes ship out of the box: main (default, spreadsheet-like), horizon (clean, analytics-focused), classic (legacy replacement).
CSS file approach (simplest)
Import the base stylesheet plus a theme:
import 'handsontable/styles/handsontable.min.css';
import 'handsontable/styles/ht-theme-main.min.css';
Then set the theme on the grid:
<HotTable themeName="ht-theme-main" /* ... */ />
Dark mode:
- System preference (auto):
themeName="ht-theme-main-dark-auto" - Forced dark:
themeName="ht-theme-main-dark"
Replace main with horizon or classic for other themes.
Theme API approach (runtime switching)
import { mainTheme, registerTheme } from 'handsontable/themes';
const theme = registerTheme(mainTheme)
.setColorScheme('auto') // 'light' | 'dark' | 'auto'
.setDensityType('comfortable'); // 'compact' | 'default' | 'comfortable'
Then pass it as a prop:
<HotTable theme={theme} /* ... */ />
CDN theme files
<!-- Theme CSS (includes light and dark mode support) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/handsontable/styles/ht-theme-main.min.css" />
In v17, dark mode is controlled via themeName or the Theme API — there are no separate -dark or
-dark-auto CSS files. Load the base theme CSS above and set the mode at runtime.
For CSS variable customization, the Theme Builder, Figma design system, and 200+ design tokens, see:
Common Configuration Patterns
All options below are passed as props on <HotTable> (React) or in the config object (vanilla JS).
For the full options reference: https://handsontable.com/docs/react-data-grid/api/options/
Data binding
// Array of arrays
data={[['A1', 'B1'], ['A2', 'B2']]}
// Array of objects
data={[{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]}
columns={[{ data: 'id' }, { data: 'name' }]}
Docs: https://handsontable.com/docs/react-data-grid/binding-to-data/
Column & row headers
colHeaders={['ID', 'Name', 'Price']} // custom labels, or {true} for A, B, C...
rowHeaders={true} // 1, 2, 3... or pass an array
Column types (cell types)
Set via the columns array or cells function. Built-in types:
text(default),numeric,date,time,checkbox,select,dropdown,autocomplete,password,handsontable(nested grid),multiselect(v17+)
columns={[
{ data: 'name', type: 'text' },
{ data: 'price', type: 'numeric', numericFormat: { pattern: '$0,0.00' } },
{ data: 'active', type: 'checkbox' },
{ data: 'category', type: 'dropdown', source: ['A', 'B', 'C'] },
]}
Full cell types reference: https://handsontable.com/docs/react-data-grid/cell-type/
Sorting & filtering
columnSorting={true} // single-column sort
// or
multiColumnSorting={true} // multi-column sort
filters={true} // enable column filters
dropdownMenu={true} // column header menu with filter UI
Frozen rows/columns
fixedRowsTop={1}
fixedRowsBottom={1}
fixedColumnsStart={2}
Other common options
readOnly={true} // entire grid read-only (or per-cell/column)
contextMenu={true} // right-click menu
mergeCells={[{ row: 0, col: 0, rowspan: 2, colspan: 2 }]}
manualColumnResize={true}
manualRowResize={true}
stretchH="all" // stretch columns to fill width: 'none' | 'last' | 'all'
Per-column config with HotColumn (React)
import { HotTable, HotColumn } from '@handsontable/react-wrapper';
<HotTable data={data} licenseKey="non-commercial-and-evaluation">
<HotColumn title="Name" data="name" />
<HotColumn title="Active" data="active" type="checkbox" />
</HotTable>
Docs: https://handsontable.com/docs/react-data-grid/hot-column/
HyperFormula Integration (Formulas Plugin)
HyperFormula powers Handsontable's Formulas plugin, providing 400+ spreadsheet functions (SUM, AVERAGE, IF, VLOOKUP, etc.). This section covers using HyperFormula inside Handsontable only.
Install
npm install hyperformula
Note: Starting in Handsontable v18, HyperFormula will no longer be bundled. Install it separately and pass it to the Formulas plugin.
CDN
<script src="https://cdn.jsdelivr.net/npm/hyperformula/dist/hyperformula.full.min.js"></script>
Simple setup (auto-created instance)
Pass the HyperFormula class directly — Handsontable creates an instance automatically:
import { HyperFormula } from 'hyperformula';
<HotTable
data={[
['10', '20', '=SUM(A1:B1)'],
['30', '40', '=SUM(A2:B2)'],
]}
formulas={{ engine: HyperFormula }}
colHeaders={true}
licenseKey="non-commercial-and-evaluation"
/>
External instance (for multi-sheet or shared engine)
Create a HyperFormula instance with the 'internal-use-in-handsontable' license key:
import { HyperFormula } from 'hyperformula';
const hfInstance = HyperFormula.buildEmpty({
licenseKey: 'internal-use-in-handsontable',
});
// Sheet 1
<HotTable
data={data1}
formulas={{ engine: hfInstance, sheetName: 'Sheet1' }}
licenseKey="non-commercial-and-evaluation"
/>
// Sheet 2 — shares the same engine, enabling cross-sheet references
<HotTable
data={data2}
formulas={{ engine: hfInstance, sheetName: 'Sheet2' }}
licenseKey="non-commercial-and-evaluation"
/>
Cross-sheet formula example: =SUM(Sheet1!A:A)
Named expressions
formulas={{
engine: HyperFormula,
namedExpressions: [
{ name: 'TAX_RATE', expression: 0.21 },
],
}}
Use in cells: =A1 * TAX_RATE
Listening for formula changes
<HotTable
formulas={{ engine: HyperFormula }}
afterFormulasValuesUpdate={(changes) => {
changes.forEach((c) => console.log(c.address, c.newValue));
}}
/>
Known limitations
- Formulas don't work with nested object data.
- Moving rows/columns with formulas requires the ManualRowMove / ManualColumnMove plugins (not IndexMapper).
getSourceData()operates on physical indexes; formulas use visual indexes.
Full guide: https://handsontable.com/docs/react-data-grid/formula-calculation/ HyperFormula functions list: https://hyperformula.handsontable.com/docs/guide/built-in-functions.html HyperFormula version compatibility table: https://handsontable.com/docs/react-data-grid/formula-calculation/#hyperformula-version-support
Server-side data with DataProvider (v17.1+)
The DataProvider plugin (new in v17.1) wires the grid up to a remote data source so rows are fetched, sorted, and mutated server-side instead of held in memory. Use it for datasets too large to load up front, or when the source of truth lives in a backend.
The dataProvider option is an object with five required keys: a row-id resolver, a paginated fetchRows callback, and three mutation callbacks. For a read-only grid, stub the mutation callbacks with async () => {} — they must still be present.
<HotTable
colHeaders={['ID', 'Name', 'Email']}
pagination={{ pageSize: 25 }}
columnSorting={true}
emptyDataState={true}
height={360}
licenseKey="non-commercial-and-evaluation"
dataProvider={{
// Required: how to extract a stable id from each row
rowId: 'id',
// Required: paginated fetch. `sort` is null when no column is sorted.
// Second arg carries an AbortSignal — pass it to fetch() so the plugin
// can cancel superseded requests when the user pages/sorts quickly.
async fetchRows({ page, pageSize, sort }, { signal }) {
const params = new URLSearchParams({ page, pageSize });
if (sort) {
// sort = { prop: string, order: 'asc' | 'desc' }
params.set('sortBy', sort.prop);
params.set('order', sort.order);
}
const res = await fetch(`/api/users?${params}`, { signal });
const { rows, totalRows } = await res.json();
return { rows, totalRows };
},
// Required. Stub with async () => {} if your grid is read-only.
// The plugin auto-refetches the current page after each callback resolves.
// See the plugin API ref for the exact payload shapes.
onRowsCreate: async () => {},
onRowsUpdate: async () => {},
onRowsRemove: async () => {},
}}
/>
Loading and error UI
The plugin fires three hooks you can wire up as <HotTable> props for loading indicators and error surfaces:
beforeDataProviderFetch(params)— fires before each fetch.params.skipLoadingis set when the plugin wants to suppress your loading indicator (e.g., during a quick refetch).afterDataProviderFetch(result)— fires after a successful fetch.afterDataProviderFetchError(error)— fires whenfetchRowsthrows or returns a rejected promise.
Companion options
The plugin is built to pair with pagination (paginates server-side), columnSorting (single-column server-side sort), and emptyDataState (loading + empty state UI). Enable all three when you use DataProvider.
Authoritative references
- Guide: https://handsontable.com/docs/react-data-grid/server-side-data-fetching/
- Recipe (REST API): https://handsontable.com/docs/react-data-grid/recipes/data-management/load-data-rest-api/
- Plugin API: https://handsontable.com/docs/react-data-grid/api/data-provider/
Notifications (v17.1+)
The Notification plugin (new in v17.1) shows non-blocking toast notifications anchored to the grid — useful for confirming saves, surfacing validation errors, or signaling background sync state. Enable with notifications: true and trigger via hot.getPlugin('notifications').showMessage(...). See the plugin guide for placement, severity levels, and auto-dismiss timing: https://handsontable.com/docs/react-data-grid/notification/
Events & Hooks
Handsontable hooks are passed as props on <HotTable>:
<HotTable
afterChange={(changes, source) => {
if (source !== 'loadData') {
console.log('Cell changed:', changes);
}
}}
beforeChange={(changes, source) => {
// Return false to cancel the edit
}}
afterSelection={(row, col, row2, col2) => {
console.log('Selected:', row, col, 'to', row2, col2);
}}
/>
There are 100+ hooks available. Full reference: https://handsontable.com/docs/react-data-grid/api/hooks/
Guide: https://handsontable.com/docs/react-data-grid/events-and-hooks/
Accessing the Instance (Ref)
Use a ref to call Handsontable's core methods:
import { useRef } from 'react';
const MyGrid = () => {
const hotRef = useRef(null);
const handleClick = () => {
const hot = hotRef.current?.hotInstance;
if (hot) {
console.log(hot.getData()); // get all data
hot.selectCell(0, 0); // select a cell
}
};
return (
<>
<HotTable ref={hotRef} /* ...options */ />
<button onClick={handleClick}>Get Data</button>
</>
);
};
Core API reference: https://handsontable.com/docs/react-data-grid/api/core/
Performance & Large Datasets
Handsontable virtualizes rendering automatically — only visible rows and columns are in the DOM, so
grids with 10k–100k+ rows perform well out of the box. For bulk programmatic updates, wrap mutations
in batch() to defer re-rendering until all changes are applied:
hotRef.current.hotInstance.batch(() => {
// set many cells, add rows, etc.
});
For further tuning (disabling auto-size, reducing plugin overhead), see:
Recipes (v17+)
Handsontable v17 introduced a Recipes section in the docs — ready-made patterns for common use cases (data validation workflows, dynamic column generation, etc.). Check the recipes index before building something from scratch: https://handsontable.com/docs/react-data-grid/recipes/
Common Pitfalls
- Forgetting
height: The grid won't render without aheightprop. Use"auto", a pixel value, or a CSS string. - Not filtering
loadDatainafterChange: TheafterChangehook fires on initial data load withsource === 'loadData'. Always check the source to avoid infinite loops when syncing changes back to state. - Using the old wrapper packages: v17 removed
@handsontable/reactand@handsontable/angular. Use@handsontable/react-wrapperand@handsontable/angular-wrapper. - Using legacy CSS imports:
handsontable.full.min.csswas removed in v17. Usehandsontable/styles/handsontable.min.cssplus a theme file. - Formulas with nested object data: HyperFormula formulas don't work when
datais an array of nested objects — use flat objects or arrays of arrays. - ExportFile
columnHeadersrenamed: In v17.1 the ExportFile plugin'scolumnHeadersoption was renamed tocolHeadersto match the table-level option. Update anyexportAsString/exportAsBlob/downloadFilecalls that passcolumnHeaders: ....
Version Awareness
Handsontable docs are versioned. The latest docs live at /docs/react-data-grid/ (which redirects
to the current version). To link to a specific version, use /docs/17.0/react-data-grid/.
When helping a user, check which version they are on — breaking changes between major versions are common. Point them to the relevant migration guide if they're upgrading.
For the full organized directory of documentation links, read references/docs-map.md in this
skill's folder.
v17.1 changes (latest, May 2026)
- New plugins: DataProvider (server-side row loading via
dataProvideroption), Notification (toast notifications). - NestedHeaders rowspan: column headers can now span multiple header rows.
- ExportFile: XLSX export added; the
columnHeadersoption was renamed tocolHeaders(see Common Pitfalls). - Angular wrapper: modernized for Angular 17–19; simpler setup, fewer deps.
- Touch: long-press now opens the context menu on touch devices.
- TypeScript:
dateFormatoption now acceptsIntl.DateTimeFormatOptions. - No removals or deprecations in v17.1.
v17.0 Breaking Changes
- Removed legacy wrapper packages (
@handsontable/react,@handsontable/angular). Use@handsontable/react-wrapperand@handsontable/angular-wrapper. - Removed legacy CSS (
handsontable.full.min.css). Usehandsontable/styles/imports. - Removed core-js from dependencies.
- Removed the PersistentState plugin.
- Deprecated bundled HyperFormula (will require separate install in v18).
- Deprecated numbro.js, Pikaday, moment.js, DOMPurify — use native alternatives.
Migration guide: https://handsontable.com/docs/react-data-grid/migration-from-16.2-to-17.0/