# Handsontable Documentation > Full guides corpus for LLM consumption. One canonical copy per page (JavaScript > variant, or the page's own framework for framework-specific guides); the other > variants exist at the same slug under their own framework prefix. Each page is > also served individually at /docs/_md/{framework}/{slug}.md. --- # Introduction URL: https://handsontable.com/docs/javascript-data-grid/ Handsontable DataGrid is a JavaScript component with a spreadsheet-like look and feel. This page explains what it does, who uses it, and when it is the right tool for your project. [[toc]] ## Getting started 🚀 Use Handsontable with plain JavaScript, TypeScript, or your favorite framework. This guide will walk you through the basics, from installation to creating your first data grid.
- [React](@/react/guides/getting-started/installation/installation.md) - [JavaScript](@/javascript/guides/getting-started/installation/installation.md) - [Angular](@/angular/guides/getting-started/installation/installation.md) - [Vue 3](@/vue/guides/getting-started/installation/installation.md)
## Examples
- [Angular](https://handsontable.com/codesandbox-vm?example-dir=angular&handsontable-version=18.1.2) - [Vanilla JS](https://handsontable.com/codesandbox-vm?example-dir=javascript&handsontable-version=18.1.2) - [React TS](https://handsontable.com/codesandbox-vm?example-dir=react&handsontable-version=18.1.2) - [React JS](https://handsontable.com/codesandbox-vm?example-dir=react-js&handsontable-version=18.1.2) - [TypeScript](https://handsontable.com/codesandbox-vm?example-dir=typescript&handsontable-version=18.1.2) - [Vue 3](https://handsontable.com/codesandbox-vm?example-dir=vue&handsontable-version=18.1.2)
Examples with SSR (Server Side Rendering):
- [Next.js](https://handsontable.com/codesandbox-vm?example-dir=next.js&handsontable-version=18.1.2) - [Astro](https://handsontable.com/codesandbox-vm?example-dir=astro&handsontable-version=18.1.2) - [Remix](https://handsontable.com/codesandbox-vm?example-dir=remix&handsontable-version=18.1.2) - [Nuxt](https://handsontable.com/codesandbox-vm?example-dir=nuxt&handsontable-version=18.1.2)
## What can I use Handsontable for? Think of Handsontable as an extensible framework that empowers you to quickly build tabular, data-oriented user interfaces tailored to your specific needs. With Handsontable, developers can efficiently tackle real-life problems by leveraging its flexibility and customization options. Examples include: - In an internal financial application, an editable Handsontable-based grid simplifies importing hand-picked data from Excel and Google Sheets. - In a construction company's software, an interactive data table built with Handsontable helps users modify codes and standards tables. - At a hospital, Handsontable helps track and manage supplies. - A mobile game company uses Handsontable to streamline certain aspects of development. - In project management software, Handsontable allows for managers to collect weekly feedback from the team and customers. Discover more about how Handsontable is used in different industries by visiting our [Successful Customers](https://handsontable.com/customers/) page. ## Types of software Handsontable's built-in features make it a perfect fit for applications across different types of software. - Feature-rich spreadsheet software - Inventory management systems - Data modeling applications - Resource planning software (ERP) - Construction of digital platforms - Commission automation tools - Knowledge management systems - Reporting platforms - Data management systems ## Join our Community 🙌 You are welcome to join our GitHub community. Discuss new releases, propose features, and report bugs on:
- [Developers' forum](https://forum.handsontable.com/)
## Technical support Implementing Handsontable requires a certain level of front-end development skills. If you need help and your support plan is active, contact our [technical support](https://handsontable.com/contact?category=technical_support) or [report an issue on GitHub](https://github.com/handsontable/handsontable/issues/new/choose) ## Stay in the loop
- [Changelog](@/guides/upgrade-and-migration/changelog-18/changelog-18.md) - [Blog](https://handsontable.com/blog) - [X](https://x.com/handsontable) - [LinkedIn](https://linkedin.com/company/handsontable)
## Related - [Installation](@/guides/getting-started/installation/installation.md) -- add Handsontable to your project using a package manager or CDN. - [Demo](@/guides/getting-started/demo/demo.md) -- try out the grid's core features interactively before you build. --- # Demo URL: https://handsontable.com/docs/javascript-data-grid/demo/ Explore Handsontable core features in this interactive demo. Click cells, sort columns, and use the context menu to see what the grid can do. [[toc]] ::: only-for javascript ::: example-without-tabs #example @[code collapse={9-108}](@/content/guides/getting-started/demo/javascript/example.js) ::: ::: ::: only-for react ::: example-without-tabs #example2 :react @[code](@/content/guides/getting-started/demo/react/example2.html) @[code](@/content/guides/getting-started/demo/react/example2.css) @[code](@/content/guides/getting-started/demo/react/example2.jsx) @[code](@/content/guides/getting-started/demo/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/getting-started/demo/angular/example1.ts) @[code](@/content/guides/getting-started/demo/angular/example1.html) ::: ::: ::: only-for vue ::: example-without-tabs #example3 :vue @[code](@/content/guides/getting-started/demo/vue/example3.vue) ::: ::: ## Find the code on GitHub
- [JavaScript demo app](https://github.com/handsontable/handsontable/tree/prod-docs/18.1/examples/next/docs/js/demo/) - [TypeScript demo app](https://github.com/handsontable/handsontable/tree/prod-docs/18.1/examples/next/docs/ts/demo/) - [Angular demo app](https://github.com/handsontable/handsontable/tree/prod-docs/18.1/examples/next/docs/angular-wrapper/demo/) - [React demo app](https://github.com/handsontable/handsontable/tree/prod-docs/18.1/examples/next/docs/react-wrapper/demo/) - [Vue demo app](https://github.com/handsontable/handsontable/tree/prod-docs/18.1/examples/next/docs/vue3/demo/)
## Try out the demo's features Explore the demo and discover Handsontable's most popular features:
- [Context menu](@/guides/accessories-and-menus/context-menu/context-menu.md) - [Dropdown cell type](@/guides/cell-types/dropdown-cell-type/dropdown-cell-type.md) - [Column groups](@/guides/columns/column-groups/column-groups.md) - [Column menu](@/guides/accessories-and-menus/column-menu/column-menu.md) - [Column filter](@/guides/columns/column-filter/column-filter.md) - [Column hiding](@/guides/columns/column-hiding/column-hiding.md) - [Rows sorting](@/guides/rows/rows-sorting/rows-sorting.md)
## Edit the demo's source code You can: - View the complete source code behind your framework's demo - Run your framework's demo on your local machine - Fork the directory with your framework's demo, and add changes of your own Just select your framework from the demo above. ## What you learned - Handsontable renders an interactive spreadsheet-like grid directly in the browser. - Built-in features include the context menu, dropdown cell types, column groups, column filtering, column hiding, and row sorting. - The grid is available for JavaScript, TypeScript, React, Angular, and Vue 3. ## Next steps - [Installation](@/guides/getting-started/installation/installation.md) -- add Handsontable to your own project. - [Binding to data](@/guides/getting-started/binding-to-data/binding-to-data.md) -- connect the grid to your data source. --- # Installation URL: https://handsontable.com/docs/javascript-data-grid/installation/ ::: only-for angular Install Handsontable through your preferred package manager, and control your grid through the `HotTableComponent` props. ::: ::: only-for javascript Install Handsontable through your preferred package manager, or import Handsontable's assets directly from a CDN. ::: ::: only-for react Install Handsontable through your preferred package manager, and control your grid through the `HotTable` component's props. ::: ::: only-for vue Install Handsontable through your preferred package manager, and control your grid through the `HotTable` component's props. ::: [[toc]]
::: only-for angular ## Prerequisites - [Node.js](https://nodejs.org/) version 18 or newer - Angular CLI version 16 or newer: `npm install -g @angular/cli` ## Install Handsontable To install Handsontable locally using a package manager, run one of these commands: ```bash npm install handsontable @handsontable/angular-wrapper ``` ```bash yarn add handsontable @handsontable/angular-wrapper ``` ```bash pnpm add handsontable @handsontable/angular-wrapper ``` ## Install Skills for Claude Code Skills for Claude Code give Claude AI deep knowledge of Handsontable's APIs, so it can build, configure, and debug your grid accurately. We recommend installing them alongside Handsontable. In Claude Code, run: ```bash /plugin marketplace add handsontable/handsontable-skills /plugin install handsontable-skills@handsontable-skills ``` For more details, see [Skills for Claude Code](@/guides/ai-tools/skills-for-claude-code/skills-for-claude-code.md). ## Configure `app.config.ts` In `app.config.ts`, register Handsontable's modules and set global configuration values via the `HOT_GLOBAL_CONFIG` token. You can modify these values at any time using `HotGlobalConfigService`, or override them per table. All properties of `HotGlobalConfig` are optional. ```ts import { ApplicationConfig, provideZoneChangeDetection } from "@angular/core"; import { HOT_GLOBAL_CONFIG, HotGlobalConfig, NON_COMMERCIAL_LICENSE, } from "@handsontable/angular-wrapper"; import { registerAllModules } from "handsontable/registry"; import { registerLanguageDictionary, enUS } from "handsontable/i18n"; registerAllModules(); registerLanguageDictionary(enUS); const globalHotConfig: HotGlobalConfig = { license: NON_COMMERCIAL_LICENSE, layoutDirection: "ltr", language: enUS.languageCode, }; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), { provide: HOT_GLOBAL_CONFIG, useValue: globalHotConfig }, ], }; ``` To reduce the size of your JavaScript bundle, [import only the modules that you need](@/guides/tools-and-building/modules/modules.md) instead of calling `registerAllModules()`. ## Use the `HotTable` component The main Handsontable component is called `HotTableComponent`. Import `HotTableModule` in your component and pass configuration via a `GridSettings` object: ```ts import { Component } from "@angular/core"; import { GridSettings, HotTableModule, } from "@handsontable/angular-wrapper"; @Component({ standalone: true, imports: [HotTableModule], template: `
`, }) export class HotTableWrapperComponent { readonly data = [ ["", "Tesla", "Volvo", "Toyota", "Ford"], ["2019", 10, 11, 12, 13], ["2020", 20, 11, 14, 13], ["2021", 30, 15, 12, 13], ["2022", 25, 20, 11, 14], ]; readonly gridSettings: GridSettings = { rowHeaders: true, colHeaders: true, height: "auto", autoWrapRow: true, autoWrapCol: true, }; } ``` ### Preview the result ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/getting-started/installation/angular/example1.ts) @[code](@/content/guides/getting-started/installation/angular/example1.html) ::: ## Supported versions of Angular `@handsontable/angular-wrapper` requires at least Angular 16. If you use a lower version of Angular, you can use the `@handsontable/angular` package instead. For more information on `@handsontable/angular`, see the [15.3 documentation](https://handsontable.com/docs/15.3/javascript-data-grid/angular-installation/). | Angular version | Handsontable wrapper | | ---------------- | ------------------------------------------------ | | Older than 16 | [@handsontable/angular](https://www.npmjs.com/package/@handsontable/angular) (deprecated) | | 16 and newer | [@handsontable/angular-wrapper](https://www.npmjs.com/package/@handsontable/angular-wrapper) | ### Troubleshooting If you're using Angular 21 or newer, please note that older versions of `@handsontable/angular-wrapper` are incompatible due to recent breaking changes in Angular. To ensure smooth integration, upgrade to `@handsontable/angular-wrapper@16.2` or later. ## Server-side rendering (SSR) Currently, `HotTable` cannot be rendered on the server-side. If your application uses SSR, render it only in the browser using the `@if` control flow block. ```ts import { isPlatformBrowser } from "@angular/common"; import { Component, inject, PLATFORM_ID } from "@angular/core"; import { GridSettings, HotTableModule } from "@handsontable/angular-wrapper"; @Component({ standalone: true, selector: "app-root", imports: [HotTableModule], templateUrl: "./app.html", styleUrl: "./app.scss", }) export class App { private platformId = inject(PLATFORM_ID); readonly isBrowser = isPlatformBrowser(this.platformId); readonly data = [ ["", "Tesla", "Volvo", "Toyota", "Ford"], ["2019", 10, 11, 12, 13], ["2020", 20, 11, 14, 13], ["2021", 30, 15, 12, 13], ["2022", 25, 20, 11, 14], ]; readonly gridSettings: GridSettings = { rowHeaders: true, colHeaders: true, height: "auto", autoWrapRow: true, autoWrapCol: true, manualRowResize: true, manualColumnResize: true, }; } ``` ```html
@if (isBrowser) { }
``` ## Result Handsontable is installed and running in your Angular application. You can now [configure options](@/guides/getting-started/configuration-options/configuration-options.md) or [import only the modules you need](@/guides/tools-and-building/modules/modules.md) to reduce your bundle size. ::: ::: only-for javascript ## Overview To start using Handsontable, follow these steps: ## Install Handsontable Get Handsontable's files in your preferred way. ### Using a package manager To install Handsontable locally using a package manager, run one of these commands: ```bash npm install handsontable ``` ```bash yarn add handsontable ``` ```bash pnpm add handsontable ``` ### Install Skills for Claude Code Skills for Claude Code give Claude AI deep knowledge of Handsontable's APIs, so it can build, configure, and debug your grid accurately. We recommend installing them alongside Handsontable. In Claude Code, run: ```bash /plugin marketplace add handsontable/handsontable-skills /plugin install handsontable-skills@handsontable-skills ``` For more details, see [Skills for Claude Code](@/guides/ai-tools/skills-for-claude-code/skills-for-claude-code.md). ### Using a CDN To get Handsontable's files from a CDN, use the following locations: - [https://cdn.jsdelivr.net/npm/handsontable/dist/handsontable.full.min.js](https://cdn.jsdelivr.net/npm/handsontable/dist/handsontable.full.min.js) ## Import Handsontable's JavaScript Import Handsontable's JavaScript into your application. ::: tip For a more optimized build, import individual parts of Handsontable's JavaScript, using [modules](@/guides/tools-and-building/modules/modules.md). ::: ### Using CommonJS or a package manager If you're using Handsontable as a CommonJS package, or as an ECMAScript module (using a package manager), import the full distribution of Handsontable as a JavaScript file. Use your bundler's preferred method of importing files. For example: ```js import Handsontable from 'handsontable'; ``` ### Using the `script` tag If you're using Handsontable as a traditional UMD package, import the full distribution of Handsontable as a minified JavaScript file. Use the `script` tag. For example, if you're loading Handsontable's JavaScript from a CDN: ```html ``` ## Create a container In your HTML, add an empty `div`, which serves as a container for your Handsontable instance. ```html
``` ## Initialize your grid Now turn your container into a data grid with sample data. ```js const container = document.querySelector('#example1'); const hot = new Handsontable(container, { data: [ ['', 'Tesla', 'Volvo', 'Toyota', 'Ford'], ['2019', 10, 11, 12, 13], ['2020', 20, 11, 14, 13], ['2021', 30, 15, 12, 13] ], rowHeaders: true, colHeaders: true, height: 'auto', autoWrapRow: true, autoWrapCol: true, licenseKey: 'non-commercial-and-evaluation' // for non-commercial use only }); ``` ### Preview the result ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/getting-started/installation/javascript/example1.js) @[code](@/content/guides/getting-started/installation/javascript/example1.ts) ::: ::: ::: only-for react ## Install Handsontable To install Handsontable locally using a package manager, run one of these commands: ```bash npm install handsontable @handsontable/react-wrapper ``` ```bash yarn add handsontable @handsontable/react-wrapper ``` ```bash pnpm add handsontable @handsontable/react-wrapper ``` ## Install Skills for Claude Code Skills for Claude Code give Claude AI deep knowledge of Handsontable's APIs, so it can build, configure, and debug your grid accurately. We recommend installing them alongside Handsontable. In Claude Code, run: ```bash /plugin marketplace add handsontable/handsontable-skills /plugin install handsontable-skills@handsontable-skills ``` For more details, see [Skills for Claude Code](@/guides/ai-tools/skills-for-claude-code/skills-for-claude-code.md). ## Register Handsontable's modules Import and register all of Handsontable's modules with a single function call: ```jsx import Handsontable from 'handsontable/base'; import { registerAllModules } from 'handsontable/registry'; registerAllModules(); ``` Or, to reduce the size of your JavaScript bundle, [import only the modules that you need](@/guides/tools-and-building/modules/modules.md). ## Use the `HotTable` component The main Handsontable component is called `HotTable`. ```jsx import { HotTable } from '@handsontable/react-wrapper'; ``` To set Handsontable's [configuration options](@/guides/getting-started/configuration-options/configuration-options.md), use `HotTable`'s props. For example: ```jsx ``` ::: tip `@handsontable/react-wrapper` requires at least React@18 and is built with functional editors and renderers components in mind. ::: ### Preview the result ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/getting-started/installation/react/example1.jsx) @[code](@/content/guides/getting-started/installation/react/example1.tsx) ::: ::: ::: only-for vue ## Install Handsontable To install Handsontable locally using a package manager, run one of these commands: ```bash npm install handsontable @handsontable/vue3 ``` ```bash yarn add handsontable @handsontable/vue3 ``` ```bash pnpm add handsontable @handsontable/vue3 ``` ## Install Skills for Claude Code Skills for Claude Code give Claude AI deep knowledge of Handsontable's APIs, so it can build, configure, and debug your grid accurately. We recommend installing them alongside Handsontable. In Claude Code, run: ```bash /plugin marketplace add handsontable/handsontable-skills /plugin install handsontable-skills@handsontable-skills ``` For more details, see [Skills for Claude Code](@/guides/ai-tools/skills-for-claude-code/skills-for-claude-code.md). ## Register Handsontable's modules Import and register all of Handsontable's modules with a single function call: ```js import { registerAllModules } from 'handsontable/registry'; registerAllModules(); ``` Or, to reduce the size of your JavaScript bundle, [import only the modules that you need](@/guides/tools-and-building/modules/modules.md). ## Use the `HotTable` component The main Handsontable component is called `HotTable`. ```js import { HotTable } from '@handsontable/vue3'; ``` To set Handsontable's [configuration options](@/guides/getting-started/configuration-options/configuration-options.md), use `HotTable`'s props. For example: ```html ``` ### Preview the result ::: example #example1 :vue3 @[code](@/content/guides/getting-started/installation/vue/example1.vue) ::: ## Supported versions of Vue `@handsontable/vue3` requires Vue 3. Vue 2 is not supported -- use [Handsontable 16.2.0](https://handsontable.com/docs/16.2/javascript-data-grid/vue-installation/) if you need Vue 2. | Handsontable version | Vue 3 version | | -------------------- | ------------------ | | `11.0.0` and lower | No Vue 3 support | | `11.1.0` and higher | `3.2.0` and higher | :::
## Result Handsontable is installed and ready to use in your project. Import it and create your first grid instance. ## Related articles **Related guides**
- [Skills for Claude Code](@/guides/ai-tools/skills-for-claude-code/skills-for-claude-code.md) - [Modules](@/guides/tools-and-building/modules/modules.md)
**Configuration options**
- [maxCols](@/api/options.md#maxcols) - [maxRows](@/api/options.md#maxrows) - [minCols](@/api/options.md#mincols) - [minRows](@/api/options.md#minrows) - [minSpareCols](@/api/options.md#minsparecols) - [minSpareRows](@/api/options.md#minsparerows) - [startCols](@/api/options.md#startcols) - [startRows](@/api/options.md#startrows)
**Hooks**
- [afterInit](@/api/hooks.md#afterinit) - [beforeInit](@/api/hooks.md#beforeinit) - [beforeInitWalkontable](@/api/hooks.md#beforeinitwalkontable) - [construct](@/api/hooks.md#construct)
--- # Configuration options URL: https://handsontable.com/docs/javascript-data-grid/configuration-options/ Configure your grid down to each column, row, and cell, using various built-in options that control Handsontable's behavior and user interface. [[toc]] ## Overview ::: only-for javascript To apply configuration options, pass them as a second argument of the Handsontable constructor, using the object literal notation: ```js import Handsontable from "handsontable"; const container = document.querySelector('#example'); const hot = new Handsontable(container, { // configuration options, in the object literal notation licenseKey: "non-commercial-and-evaluation", data: [ ['A1', 'B1', 'C1', 'D1'], ['A2', 'B2', 'C2', 'D2'], ['A3', 'B3', 'C3', 'D3'], ], width: 400, height: 300, colHeaders: true, rowHeaders: true, customBorders: true, dropdownMenu: true, multiColumnSorting: true, filters: true, manualRowMove: true, }); ``` ::: ::: only-for react To apply configuration options, pass them as individual props of the [`HotTable`](@/guides/getting-started/installation/installation.md#_4-use-the-hottable-component) or [`HotColumn`](@/guides/columns/react-hot-column/react-hot-column.md) components. ```jsx ``` ::: ::: only-for angular To apply configuration options, pass them as `GridSettings` object to [`HotTableComponent`](@/guides/getting-started/installation/installation.md#use-the-hottable-component). ```ts import { GridSettings } from "@handsontable/angular-wrapper"; data = [ ["A1", "B1", "C1", "D1"], ["A2", "B2", "C2", "D2"], ["A3", "B3", "C3", "D3"], ]; gridSettings: GridSettings = { autoWrapRow: true, autoWrapCol: true, width: 400, height: 300, colHeaders: true, rowHeaders: true, customBorders: true, dropdownMenu: true, multiColumnSorting: true, filters: true, manualRowMove: true, }; ``` ```html ``` ::: ::: only-for vue To apply configuration options in Vue 3, pass them inside a `hotSettings` ref bound to the `HotTable` component with the `:settings` prop: ```vue ``` ::: Depending on your needs, you can apply configuration options to different elements of your grid, such as: - [The entire grid](#set-grid-options) - [Individual columns](#set-column-options) - [Individual rows](#set-row-options) - [Individual cells](#set-cell-options) - [Individual grid elements, based on any logic you implement](#implement-custom-logic) For the full list of available configuration options, see the [configuration options' API reference](@/api/options.md). ### Cascading configuration Handsontable's configuration cascades down: - From the top-level grid options ([`GlobalMeta`](https://github.com/handsontable/handsontable/blob/master/handsontable/src/dataMap/metaManager/metaLayers/globalMeta.js)) - Through the mid-level column options ([`ColumnMeta`](https://github.com/handsontable/handsontable/blob/master/handsontable/src/dataMap/metaManager/metaLayers/columnMeta.js)) - To the bottom-level cell options ([`CellMeta`](https://github.com/handsontable/handsontable/blob/master/handsontable/src/dataMap/metaManager/metaLayers/cellMeta.js)) When you modify the mid-level column options (using the [`columns`](@/api/options.md#columns) option): - The options that you change overwrite the top-level grid options. - The options that you change cascade down to the bottom-level cell options. - Any unchanged options are inherited from the top-level grid options. When you modify the bottom-level cell options (using the [`cell`](@/api/options.md#cell) option): - The options that you change overwrite the top-level grid options. - The options that you change overwrite the mid-level column options. - Any unchanged options are inherited from the mid-level column options or the top-level grid options. When you modify any options with the [`cells`](@/api/options.md#cells) function, the changes overwrite all other options. Unless an option's [reference entry](@/api/options.md) states otherwise, you can set it at any cascading level - the grid level, the [`columns`](@/api/options.md#columns) level, the [`cells`](@/api/options.md#cells) level, and the [`cell`](@/api/options.md#cell) level. An option marked as grid-level only has no effect when you set it per column or per cell. ::: tip The [`cells`](@/api/options.md#cells) option is a function invoked before Handsontable's [rendering cycle](@/guides/optimization/batch-operations/batch-operations.md). Implemented incorrectly, it can slow Handsontable down. Use the [`cells`](@/api/options.md#cells) option only if the [`cell`](@/api/options.md#cell) option, the [`columns`](@/api/options.md#columns) option, and the [`setCellMeta()`](#change-cell-options) method don't meet your needs. ::: For more details on Handsontable's cascading configuration, see the [MetaManager class](https://github.com/handsontable/handsontable/blob/master/handsontable/src/dataMap/metaManager/index.js). To read configuration at runtime, use the method that matches the level you need: - [`getSettings()`](@/api/core.md#getsettings) returns global grid settings only. - [`getColumnMeta()`](@/api/core.md#getcolumnmeta) returns column-level meta for a given column. - [`getCellMeta()`](@/api/core.md#getcellmeta) returns the effective merged meta for a given cell. ### Plugin options Configuration options come from:
- [Core](@/api/core.md) - [Plugins](@/api/plugins.md) - [Hooks](@/api/hooks.md)
If you use Handsontable through [modules](@/guides/tools-and-building/modules/modules.md): to use an option that comes from a Handsontable plugin, you need to import and register that plugin when initializing your Handsontable instance. To find out if an option comes from a plugin, check the `Category` label in the [configuration options' API reference](@/api/options.md). ## Set grid options ::: only-for javascript To apply configuration options to the entire grid, pass your options as a second argument of the Handsontable constructor, using the object literal notation. ::: ::: only-for react To apply configuration options to the entire grid, pass your options as individual props of the [`HotTable`](@/guides/getting-started/installation/installation.md#_4-use-the-hottable-component) or [`HotColumn`](@/guides/columns/react-hot-column/react-hot-column.md) components. ::: ::: only-for angular To apply configuration options to the entire grid, pass your options as `GridSettings` object to [`HotTableComponent`](@/guides/getting-started/installation/installation.md#use-the-hottable-component). ::: For example, to set the entire grid's [width](@/api/options.md#width) and [height](@/api/options.md#height): ::: only-for javascript ```js const hot = new Handsontable(container, { // top-level grid options that apply to the entire grid width: 400, height: 300 }); ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts import { GridSettings } from "@handsontable/angular-wrapper"; gridSettings: GridSettings = { width: 400, height: 300, }; ``` ```html ``` ::: ::: only-for vue ```ts const hotSettings = ref({ // top-level grid options that apply to the entire grid width: 400, height: 300, }); ``` ```html ``` ::: ### Example To configure each cell in the grid as read-only, apply the [`readOnly`](@/api/options.md#readonly) option as a top-level grid option. The top-level grid options cascade down: - To the mid-level column options - To the bottom-level cell options As a result, each cell in the grid is read-only: ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/getting-started/configuration-options/javascript/example1.js) @[code](@/content/guides/getting-started/configuration-options/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/getting-started/configuration-options/react/example1.jsx) @[code](@/content/guides/getting-started/configuration-options/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/getting-started/configuration-options/angular/example1.ts) @[code](@/content/guides/getting-started/configuration-options/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue --js 1 @[code](@/content/guides/getting-started/configuration-options/vue/example1.vue) ::: ::: ## Set column options To apply configuration options to an individual column (or a range of columns), use the [`columns`](@/api/options.md#columns) option. ::: only-for javascript ```js const hot = new Handsontable(container, { columns: [ {}, {}, // column options, apply to each cell of the third (by physical index) column { readOnly: true, }, ], }); ``` ::: ::: only-for react ```jsx ``` Alternatively, you can use the [`HotColumn`](@/guides/columns/react-hot-column/react-hot-column.md) component to configure columns declaratively: ```jsx ``` ::: ::: only-for angular ```ts import { GridSettings } from "@handsontable/angular-wrapper"; gridSettings: GridSettings = { columns: [ { width: 100 }, // column options for the first (by physical index) column { width: 100 }, // column options for the second (by physical index) column { width: 100 }, // column options for the third (by physical index) column ], }; ``` ```html ``` ::: ::: only-for vue ```ts const hotSettings = ref({ columns: [ { width: 100 }, // column options for the first (by physical index) column { width: 100 }, // column options for the second (by physical index) column { width: 100 }, // column options for the third (by physical index) column ], }); ``` ```html ``` ::: ### Example In the example below, the [`columns`](@/api/options.md#columns) option is set to a function. The function applies the [`readOnly: true`](@/api/options.md#readonly) option to each column that has a physical index of either `2` or `8`. The modified mid-level column options: - Overwrite the top-level grid options - Cascade down to the bottom-level cell options As a result, each cell in the third and ninth columns is read-only: ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/getting-started/configuration-options/javascript/example2.js) @[code](@/content/guides/getting-started/configuration-options/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/getting-started/configuration-options/react/example2.jsx) @[code](@/content/guides/getting-started/configuration-options/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/getting-started/configuration-options/angular/example2.ts) @[code](@/content/guides/getting-started/configuration-options/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue --js 1 @[code](@/content/guides/getting-started/configuration-options/vue/example2.vue) ::: ::: ## Set row options To apply configuration options to an individual row (or a range of rows), use the [`cells`](@/api/options.md#cells) option. Any options modified through [`cells`](@/api/options.md#cells) overwrite all other options. ::: only-for javascript The function can take three arguments:
- `row`: a row coordinate (a physical index) - `col`: a column coordinate (a physical index) - `prop`: if your [`data`](@/api/options.md#data) is an [array of objects](@/guides/getting-started/binding-to-data/binding-to-data.md#array-of-objects), `prop` is a property name for a column's data source object.
If your [`data`](@/api/options.md#data) is an [array of arrays](@/guides/getting-started/binding-to-data/binding-to-data.md#array-of-arrays), `prop` is the same as `col`. Inside the `cells` function, `this` is the cell meta object, and `this.instance` is the Handsontable instance. Use `this.instance` to call core API methods -- for example, `this.instance.toVisualRow(row)`. ```js const hot = new Handsontable(container, { // the `cells` options overwrite all other options cells(row, col, prop) { if (row === 1 || row === 4) { return { // row options, which apply to each cell of the second row // and to each cell of the fifth row readOnly: true, }; } } }); ``` ::: ::: only-for react The function can take three arguments:
- `row`: a row coordinate (a physical index) - `col`: a column coordinate (a physical index) - `prop`: if your [`data`](@/api/options.md#data) is an [array of objects](@/guides/getting-started/binding-to-data/binding-to-data.md#array-of-objects), `prop` is a property name for a column's data source object.
If your [`data`](@/api/options.md#data) is an [array of arrays](@/guides/getting-started/binding-to-data/binding-to-data.md#array-of-arrays), `prop` is the same as `col`. ```jsx { if (row === 1 || row === 4) { return { // row options, which apply to each cell of the second row // and to each cell of the fifth row readOnly: true, }; } }}/> ``` The arrow function passed to `cells` does not bind `this`, so `this.instance` is not available inside it. To access the Handsontable instance, use the component's ref: `hotTableRef.current.hotInstance`. ::: ::: only-for angular The function can take three arguments:
- `row`: a row coordinate (a physical index) - `col`: a column coordinate (a physical index) - `prop`: if your [`data`](@/api/options.md#data) is an [array of objects](@/guides/getting-started/binding-to-data/binding-to-data.md#array-of-objects), `prop` is a property name for a column's data source object.
If your [`data`](@/api/options.md#data) is an [array of arrays](@/guides/getting-started/binding-to-data/binding-to-data.md#array-of-arrays), `prop` is the same as `col`. ```ts import { GridSettings } from "@handsontable/angular-wrapper"; gridSettings: GridSettings = { cells: (row, col, prop) => { if (row === 1 || row === 4) { return { // row options, which apply to each cell of the second row // and to each cell of the fifth row readOnly: true, }; } }, }; ``` ```html ``` The arrow function passed to `cells` does not bind `this`, so `this.instance` is not available inside it. To access the Handsontable instance, use the component's ref: `hotTable.hotInstance`. ::: ::: only-for vue ```ts const hotSettings = ref({ cells(row, col, prop) { if (row === 1 || row === 4) { return { // row options, which apply to each cell of the second row // and to each cell of the fifth row readOnly: true, }; } }, }); ``` ```html ``` Because the `cells` function above is a shorthand method, `this` is the cell meta object and `this.instance` is the Handsontable instance -- for example, `this.instance.toVisualRow(row)`. An arrow function (`cells: () => {}`) does not bind `this`; in that case, access the instance via the component's ref: `hotRef.value?.hotInstance`. ::: ### Example In the example below, the [`cells`](@/api/options.md#cells) option sets each cell in the first and fourth row as [`readOnly`](@/api/options.md#readonly). Options modified through [`cells`](@/api/options.md#cells) overwrite all other options. ::: only-for javascript ::: example #example3 --js 1 --ts 2 @[code](@/content/guides/getting-started/configuration-options/javascript/example3.js) @[code](@/content/guides/getting-started/configuration-options/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/getting-started/configuration-options/react/example3.jsx) @[code](@/content/guides/getting-started/configuration-options/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/getting-started/configuration-options/angular/example3.ts) @[code](@/content/guides/getting-started/configuration-options/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue --js 1 @[code](@/content/guides/getting-started/configuration-options/vue/example3.vue) ::: ::: ## Set cell options To apply configuration options to individual cells, use the [`cell`](@/api/options.md#cell) option. Each entry's `row` and `col` are visual indexes (unlike the [`cells`](@/api/options.md#cells) option, whose `row` and `column` are physical indexes). ::: only-for javascript ```js const hot = new Handsontable(container, { cell: [ { // cell options, apply only to a cell with coordinates (0, 0) row: 0, col: 0, readOnly: true, }, { // cell options, apply only to a cell with coordinates (1, 1) row: 1, col: 1, readOnly: true, } ], autoWrapRow: true, autoWrapCol: true, }); ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts import { GridSettings } from "@handsontable/angular-wrapper"; gridSettings: GridSettings = { cell: [ { // bottom-level cell options overwrite the top-level grid options // apply only to a cell with coordinates (0, 0) row: 0, col: 0, }, { // bottom-level cell options overwrite the top-level grid options // apply only to a cell with coordinates (1, 1) row: 1, col: 1, }, ], }; ``` ```html ``` ::: ::: only-for vue ```ts const hotSettings = ref({ cell: [ { // cell options, apply only to a cell with coordinates (0, 0) row: 0, col: 0, readOnly: true, }, { // cell options, apply only to a cell with coordinates (1, 1) row: 1, col: 1, readOnly: true, }, ], autoWrapRow: true, autoWrapCol: true, }); ``` ```html ``` ::: ### Example In the example below, the [`cell`](@/api/options.md#cell) option sets cell `A1`(`0, 0`) and cell `B2`(`1, 1`) as [`readOnly`](@/api/options.md#readonly). The modified [`cell`](@/api/options.md#cell) options: - Overwrite the top-level grid options - Overwrite mid-level column options ::: only-for javascript ::: example #example4 --js 1 --ts 2 @[code](@/content/guides/getting-started/configuration-options/javascript/example4.js) @[code](@/content/guides/getting-started/configuration-options/javascript/example4.ts) ::: ::: ::: only-for react ::: example #example4 :react --js 1 --ts 2 @[code](@/content/guides/getting-started/configuration-options/react/example4.jsx) @[code](@/content/guides/getting-started/configuration-options/react/example4.tsx) ::: ::: ::: only-for angular ::: example #example4 :angular --ts 1 --html 2 @[code](@/content/guides/getting-started/configuration-options/angular/example4.ts) @[code](@/content/guides/getting-started/configuration-options/angular/example4.html) ::: ::: ::: only-for vue ::: example #example4 :vue --js 1 @[code](@/content/guides/getting-started/configuration-options/vue/example4.vue) ::: ::: ### Read cell options When Handsontable is running, you can check a cell's current options, using the [`getCellMeta()`](@/api/core.md#getcellmeta) method. The [`getCellMeta()`](@/api/core.md#getcellmeta) method returns an object with: - All built-in options (stored in the [`CellMeta`](https://github.com/handsontable/handsontable/blob/master/handsontable/src/dataMap/metaManager/metaLayers/cellMeta.js) [prototype](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Objects/Object_prototypes)) - Any options you add For example: ::: only-for javascript ```js import Handsontable from 'handsontable'; const container = document.querySelector('#example'); const hot = new Handsontable(container, { // top-level grid options that apply to the entire grid data: [ ['A1', 'B1', 'C1', 'D1'], ['A2', 'B2', 'C2', 'D2'], ['A3', 'B3', 'C3', 'D3'], ], licenseKey: 'non-commercial-and-evaluation', width: 'auto', height: 'auto', rowHeaders: true, colHeaders: true, // in the top-level grid options, all cells are read-only readOnly: false, cell: [ { // bottom-level cell options overwrite the top-level grid options // apply only to a cell with coordinates (1, 1) row: 1, col: 1, readOnly: true, } ] }); // for cell (0, 0), the `readOnly` option is the default (`false`) // returns `false` hot.getCellMeta(0, 0).readOnly; // for cell (1, 1), the `cell` option overwrote the default `readOnly` value // returns `true` hot.getCellMeta(1, 1).readOnly; ``` ::: ::: only-for react ```jsx // Consider the HotTable component with the `cell` option declared: ; // for cell (0, 0), the `readOnly` option is the default (`false`) // returns `false` hot.getCellMeta(0, 0).readOnly; // for cell (1, 1), the `cell` option overwrote the default `readOnly` value // returns `true` hot.getCellMeta(1, 1).readOnly; ``` ::: ::: only-for angular ```ts @ViewChild(HotTableComponent, { static: false }) readonly hotTable!: HotTableComponent; import { GridSettings } from "@handsontable/angular-wrapper"; // Consider the HotTable component with the `cell` option declared: gridSettings: GridSettings = { cell: [ { row: 1, col: 1, readOnly: true, }, ] }; // for cell (0, 0), the `readOnly` option is the default (`false`) // returns `false` hotTable.hotInstance.getCellMeta(0, 0).readOnly; // for cell (1, 1), the `cell` option overwrote the default `readOnly` value // returns `true` hotTable.hotInstance.getCellMeta(1, 1).readOnly; ``` ```html ``` ::: ::: only-for vue ```ts // Access the Handsontable instance via the template ref: const hot = hotRef.value?.hotInstance; // for cell (0, 0), the `readOnly` option is the default (`false`) // returns `false` hot?.getCellMeta(0, 0).readOnly; // for cell (1, 1), the `cell` option overwrote the default `readOnly` value // returns `true` hot?.getCellMeta(1, 1).readOnly; ``` ::: ### Change cell options When Handsontable is running, you can change the initial cell options, using the [`setCellMeta()`](@/api/core.md#setcellmeta) method. For example: ::: only-for javascript ```js import Handsontable from 'handsontable'; const container = document.querySelector('#example'); const hot = new Handsontable(container, { // top-level grid options that apply to the entire grid data: [ ['A1', 'B1', 'C1', 'D1'], ['A2', 'B2', 'C2', 'D2'], ['A3', 'B3', 'C3', 'D3'], ], licenseKey: 'non-commercial-and-evaluation', width: 'auto', height: 'auto', rowHeaders: true, colHeaders: true, }); // changes the `readOnly` option of cell (1, 1) back to `false` hot.setCellMeta(1, 1, 'readOnly', false); // returns `false` hot.getCellMeta(1, 1).readOnly; ``` ::: ::: only-for react ```jsx // changes the `readOnly` option of cell (1, 1) back to `false` hot.setCellMeta(1, 1, 'readOnly', false); // returns `false` hot.getCellMeta(1, 1).readOnly; ``` ::: ::: only-for angular ```ts @ViewChild(HotTableComponent, { static: false }) readonly hotTable!: HotTableComponent; import { GridSettings } from "@handsontable/angular-wrapper"; // Consider the HotTable component with the `cell` option declared: gridSettings: GridSettings = { cell: [ { row: 1, col: 1, readOnly: true, }, ] }; // changes the `readOnly` option of cell (1, 1) back to `false` hotTable.hotInstance.setCellMeta(1, 1, "readOnly", false); // returns `false` hotTable.hotInstance.getCellMeta(1, 1).readOnly; ``` ```html ``` ::: ::: only-for vue ```ts const hot = hotRef.value?.hotInstance; // changes the `readOnly` option of cell (1, 1) back to `false` hot?.setCellMeta(1, 1, 'readOnly', false); // returns `false` hot?.getCellMeta(1, 1).readOnly; ``` ::: [`setCellMeta()`](@/api/core.md#setcellmeta) updates a cell's metadata but doesn't repaint the grid on its own. To make a visual change appear -- such as a new `className`, `type`, or `readOnly` state -- call [`render()`](@/api/core.md#render) afterward. If you change several cells at once, wrap the calls in [`batch()`](@/api/core.md#batch) so the grid renders only once. ## Implement custom logic You can apply configuration options to individual grid elements (columns, rows, cells), based on any logic you implement, using the [`cells`](@/api/options.md#cells) option. The [`cells`](@/api/options.md#cells) option overwrites all other options. ::: only-for javascript The function can take three arguments:
- `row`: a row coordinate (a physical index) - `col`: a column coordinate (a physical index) - `prop`: if your [`data`](@/api/options.md#data) is an [array of objects](@/guides/getting-started/binding-to-data/binding-to-data.md#array-of-objects), `prop` is a property name for a column's data source object.
If your [`data`](@/api/options.md#data) is an [array of arrays](@/guides/getting-started/binding-to-data/binding-to-data.md#array-of-arrays), `prop` is the same as `col`. Inside the `cells` function, `this` is the cell meta object, and `this.instance` is the Handsontable instance. Use `this.instance` to call core API methods -- for example, `this.instance.toVisualRow(row)`. ```js const hot = new Handsontable(container, { cells(row, col) { if ((row === 1 || row === 5) && col === 1) { return { readOnly: true, }; } } }); ``` ::: ::: only-for react The function can take three arguments:
- `row`: a row coordinate (a physical index) - `col`: a column coordinate (a physical index) - `prop`: if your [`data`](@/api/options.md#data) is an [array of objects](@/guides/getting-started/binding-to-data/binding-to-data.md#array-of-objects), `prop` is a property name for a column's data source object.
If your [`data`](@/api/options.md#data) is an [array of arrays](@/guides/getting-started/binding-to-data/binding-to-data.md#array-of-arrays), `prop` is the same as `col`. ```jsx { if ((row === 1 || row === 5) && col === 1) { return { readOnly: true, }; } }} /> ``` The arrow function passed to `cells` does not bind `this`, so `this.instance` is not available inside it. To access the Handsontable instance, use the component's ref: `hotTableRef.current.hotInstance`. ::: ::: only-for angular The function can take three arguments:
- `row`: a row coordinate (a physical index) - `col`: a column coordinate (a physical index) - `prop`: if your [`data`](@/api/options.md#data) is an [array of objects](@/guides/getting-started/binding-to-data/binding-to-data.md#array-of-objects), `prop` is a property name for a column's data source object.
If your [`data`](@/api/options.md#data) is an [array of arrays](@/guides/getting-started/binding-to-data/binding-to-data.md#array-of-arrays), `prop` is the same as `col`. ```ts import { GridSettings } from "@handsontable/angular-wrapper"; gridSettings: GridSettings = { cells: (row, col) => { if ((row === 1 || row === 5) && col === 1) { return { readOnly: true, }; } }, }; ``` ```html ``` The arrow function passed to `cells` does not bind `this`, so `this.instance` is not available inside it. To access the Handsontable instance, use the component's ref: `hotTable.hotInstance`. ::: ::: only-for vue ```ts const hotSettings = ref({ cells(row, col) { if ((row === 1 || row === 5) && col === 1) { return { readOnly: true, }; } }, }); ``` ```html ``` Because the `cells` function above is a shorthand method, `this` is the cell meta object and `this.instance` is the Handsontable instance -- for example, `this.instance.toVisualRow(row)`. An arrow function (`cells: () => {}`) does not bind `this`; in that case, access the instance via the component's ref: `hotRef.value?.hotInstance`. ::: ### Example In the example below, the modified [`cells`](@/api/options.md#cells) options overwrite the top-level grid options. ::: only-for javascript ::: example #example5 --js 1 --ts 2 @[code](@/content/guides/getting-started/configuration-options/javascript/example5.js) @[code](@/content/guides/getting-started/configuration-options/javascript/example5.ts) ::: ::: ::: only-for react ```jsx // for cell (0, 0), the `readOnly` option is the default (`false`) // returns `false` hot.getCellMeta(0, 0).readOnly; // for cell (1, 1), the `cell` option overwrote the default `readOnly` value // returns `true` hot.getCellMeta(1, 1).readOnly; // changes the `readOnly` option of cell (1, 1) back to `false` hot.setCellMeta(1, 1, 'readOnly', false); // returns `false` hot.getCellMeta(1, 1).readOnly; ``` ::: ::: only-for angular ```ts // for cell (0, 0), the `readOnly` option is the default (`false`) // returns `false` hot.getCellMeta(0, 0).readOnly; // for cell (1, 1), the `cell` option overwrote the default `readOnly` value // returns `true` hot.getCellMeta(1, 1).readOnly; // changes the `readOnly` option of cell (1, 1) back to `false` hot.setCellMeta(1, 1, 'readOnly', false); // returns `false` hot.getCellMeta(1, 1).readOnly; ``` ::: ::: only-for vue ```ts const hot = hotRef.value?.hotInstance; // for cell (0, 0), the `readOnly` option is the default (`false`) // returns `false` hot?.getCellMeta(0, 0).readOnly; // for cell (1, 1), the `cell` option overwrote the default `readOnly` value // returns `true` hot?.getCellMeta(1, 1).readOnly; // changes the `readOnly` option of cell (1, 1) back to `false` hot?.setCellMeta(1, 1, 'readOnly', false); // returns `false` hot?.getCellMeta(1, 1).readOnly; ``` ::: ## Configuration example In the example below, some cells are read-only, and some cells are editable: - By default, all cells are read-only (as set in the top-level grid options). - For the first column, the mid-level column options overwrite the top-level grid options.
As a result, the first column cells are editable. - For cell `A1` (`0, 0`), the bottom-level cell options overwrite both the mid-level column options, and the top-level grid options.
As a result, cell `A1` (`0, 0`) is read-only, despite being part of the editable first column. - For cell `C3` (`3, 3`), the [`cells`](@/api/options.md#cells) option overwrites all other options.
As a result, cell `C3` (`3, 3`) is editable, despite not being part of the editable first column. ::: only-for javascript ::: example #example6 --js 1 --ts 2 @[code](@/content/guides/getting-started/configuration-options/javascript/example6.js) @[code](@/content/guides/getting-started/configuration-options/javascript/example6.ts) ::: ::: ::: only-for react ::: example #example6 :react --js 1 --ts 2 @[code](@/content/guides/getting-started/configuration-options/react/example6.jsx) @[code](@/content/guides/getting-started/configuration-options/react/example6.tsx) ::: ::: ::: only-for react ## Non-idempotent options A non-idempotent option is one that produces different results when applied multiple times. In the context of Handsontable and `` component, options like `manualColumnMove=[1, 0]` will swap columns every time they're applied - first application swaps columns, second application swaps them back, third swaps again, and so on. ### Problem Non-idempotent options like `manualColumnMove` and `manualRowMove` cause unwanted visual changes on every React re-render because they operate on visual indexes. ```jsx // Problem: Columns swap on EVERY re-render const ExampleComponent = () => { const [state, setState] = React.useState(0); return ( <> ); }; ``` ### Solution Use [`initialState`](@/api/options.md#initialstate) to apply these options only during initialization: ```jsx ``` ::: ::: only-for angular ::: example #example6 :angular --ts 1 --html 2 @[code](@/content/guides/getting-started/configuration-options/angular/example6.ts) @[code](@/content/guides/getting-started/configuration-options/angular/example6.html) ::: ::: ::: only-for vue ::: example #example6 :vue3 @[code](@/content/guides/getting-started/configuration-options/vue/example6.vue) ::: ::: ## Result Your grid now applies configuration options at the scope you specified -- grid-wide, per column, per row, or per individual cell -- using Handsontable's cascading configuration system. ## Related API reference **Configuration options**
- [List of all options](@/api/options.md) - [cell](@/api/options.md#cell) - [cells](@/api/options.md#cells) - [columns](@/api/options.md#columns)
**Core methods**
- [getCellMeta()](@/api/core.md#getcellmeta) - [getCellMetaAtRow()](@/api/core.md#getcellmetaatrow) - [getCellsMeta()](@/api/core.md#getcellsmeta) - [setCellMeta()](@/api/core.md#setcellmeta) - [setCellMetaObject()](@/api/core.md#setcellmetaobject) - [removeCellMeta()](@/api/core.md#removecellmeta) - [getSettings()](@/api/core.md#getsettings) - [removeCellMeta()](@/api/core.md#removecellmeta) - [updateSettings()](@/api/core.md#updatesettings) - [spliceCellsMeta()](@/api/core.md#splicecellsmeta)
**Hooks**
- [afterCellMetaReset](@/api/hooks.md#aftercellmetareset) - [afterUpdateSettings](@/api/hooks.md#afterupdatesettings)
--- # Grid size URL: https://handsontable.com/docs/javascript-data-grid/grid-size/ Set the width and height of the grid, using either absolute values or values relative to the parent container. [[toc]] ## Set your grid's size You need to define the grid's container as a starting point to initialize it. Usually, the `div` element becomes this container. This container should have defined dimensions as well as the rest of your layout. Handsontable supports relative units such as `%`, `rem`, `em`, `vh`, `vw`, and `px`. ### Define the size in your CSS Both `width` and `height` could be defined as inline styles or as a CSS class property. In this case, it's important to define what should be an `overflow` parent properly. Handsontable looks for the closest element with `overflow: auto` or `overflow: hidden` to use it as a scrollable container. If no such element is found, a window will be used. ::: tip Handsontable doesn't observe CSS changes for containers out of the box. If you'd like to observe it, you can define the dimensions in the configuration object or create your own observer. ::: ### Pass the size in the configuration You can pass width and height values to Handsontable as numbers or possible CSS values for the "width"/"height" properties: ::: only-for javascript ```js { width: '100px', height: '100px', } ``` or ```js { width: '75%', height: '75%', } ``` or ```js { width: 100, height: 100, } ``` ::: ::: only-for react ```jsx ``` or ```jsx ``` or ```jsx ``` ::: ::: only-for angular ```ts import { GridSettings } from "@handsontable/angular-wrapper"; gridSettings: GridSettings = { height: 100, width: 100, }; ``` or ```ts import { GridSettings } from "@handsontable/angular-wrapper"; gridSettings: GridSettings = { height: "75%", width: "75%", }; ``` or ```ts import { GridSettings } from "@handsontable/angular-wrapper"; gridSettings: GridSettings = { height: "100px", width: "100px", }; ``` ```html ``` ::: ::: only-for vue ```js const hotSettings = ref({ width: '100px', height: '100px', }); ``` or ```js const hotSettings = ref({ width: '75%', height: '75%', }); ``` or ```js const hotSettings = ref({ width: 100, height: 100, }); ``` ::: You can also pass a function to `width` and `height`. Use this when you calculate dimensions from your current layout. The function can return a number (pixels) or a CSS size string. :::: only-for javascript ```js { width() { return `${window.innerWidth - 64}px`; }, height() { return 400; }, } ``` :::: :::: only-for react ```jsx const getGridWidth = () => `${window.innerWidth - 64}px`; const getGridHeight = () => 400; ``` :::: :::: only-for angular ```ts import { GridSettings } from "@handsontable/angular-wrapper"; gridSettings: GridSettings = { width: () => `${window.innerWidth - 64}px`, height: () => 400, }; ``` ```html ``` :::: :::: only-for vue ```js const hotSettings = ref({ width: () => `${window.innerWidth - 64}px`, height: () => 400, }); ``` :::: These dimensions will be set as inline styles in a container element, and `overflow: hidden` will be added automatically. If container is a block element, then its parent has to have defined `height`. By default block element is `0px` height, so `100%` from `0px` is still `0px`. Changes called in [`updateSettings()`](@/api/core.md#updatesettings) will re-render the grid with the new properties. ### Compare size units Use the dropdown in the demo below to switch the grid's `width` and `height` between `px`, `%`, `em`, `rem`, `vh`, and `vw`, and see how the same grid responds to each unit. ::: only-for javascript ::: example #example2 --html 1 --css 2 --js 3 --ts 4 @[code](@/content/guides/getting-started/grid-size/javascript/example2.html) @[code](@/content/guides/getting-started/grid-size/javascript/example2.css) @[code](@/content/guides/getting-started/grid-size/javascript/example2.js) @[code](@/content/guides/getting-started/grid-size/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --css 1 --js 2 --ts 3 @[code](@/content/guides/getting-started/grid-size/react/example2.css) @[code](@/content/guides/getting-started/grid-size/react/example2.jsx) @[code](@/content/guides/getting-started/grid-size/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --css 1 --ts 2 --html 3 @[code](@/content/guides/getting-started/grid-size/angular/example2.css) @[code](@/content/guides/getting-started/grid-size/angular/example2.ts) @[code](@/content/guides/getting-started/grid-size/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/getting-started/grid-size/vue/example2.vue) ::: ::: ### Use `'auto'` sizing Set `height: 'auto'` to make the grid grow to match its content height. Handsontable writes `height: auto; overflow: clip;` as inline styles on the root element. No internal vertical scrollbar is created, so the page itself scrolls when the grid is taller than the viewport. ::: only-for javascript ```js { height: 'auto', } ``` You can combine it with `width: 'auto'` to let the grid follow its parent container's width: ```js { height: 'auto', width: 'auto', } ``` ::: ::: only-for react ```jsx ``` You can combine it with `width="auto"` to let the grid follow its parent container's width: ```jsx ``` ::: ::: only-for angular ```ts import { GridSettings } from "@handsontable/angular-wrapper"; gridSettings: GridSettings = { height: "auto", }; ``` You can combine it with `width: "auto"` to let the grid follow its parent container's width: ```ts import { GridSettings } from "@handsontable/angular-wrapper"; gridSettings: GridSettings = { height: "auto", width: "auto", }; ``` ::: ::: only-for vue ```js const hotSettings = ref({ height: 'auto', }); ``` You can combine it with `width: 'auto'` to let the grid follow its parent container's width: ```js const hotSettings = ref({ height: 'auto', width: 'auto', }); ``` ::: `height: 'auto'` is different from leaving `height` unset: | Setting | Inline styles on root | Scroll parent | Row virtualization | | ------------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------------ | --------------------------- | | `height: 'auto'` | `height: auto; overflow: clip;` | The page, outside the grid | Disabled, all rows render | | `height: ` | `height: px; overflow: clip;` | The grid itself | Enabled | | `height` unset | None, Handsontable does not touch the root inline styles | Nearest ancestor with `overflow: auto` or `overflow: hidden`, else the window | Depends on the scroll parent | ::: tip With `height: 'auto'`, every row is laid out in the DOM at once. Avoid this value for large datasets. Set a numeric `height` instead so that Handsontable can virtualize off-screen rows. ::: ### Troubleshooting with 100% height When the `height` option is set to 100%, there are three ways to define the container’s height. Assuming you're creating an Handsontable instance that has `100% height` and container is element with id `#example`. ```js const container = document.querySelector('#example'); const hot = new Handsontable(container, { height: '100%', // ...rest of config } ``` 1. Set a fixed height (in pixels) directly on the `example` element where Handsontable is mounted ```html
``` 2. Set a fixed height on the parent element, and then give the `example` itself a height of 100% ```html
``` 3. Use flexbox on the wrapper element to make the `example` fill the available space ```html
``` When using Flexbox, the container automatically expands to fill the available space in the flex container. This is particularly useful when you want the grid to take up all the available space within its parent. ## What if the size is not set If you don't define any dimensions, Handsontable generates as many rows and columns as needed to fill the available space. If your grid's contents don't fit in the viewport, the browser's native scrollbars are used for scrolling. For this to work properly, Handsontable's [layout direction](@/guides/internationalization/layout-direction/layout-direction.md) (e.g., [`layoutDirection: 'rtl'`](@/api/options.md#layoutdirection)) must be the same as your HTML document's layout direction (``). Otherwise, horizontal scrolling doesn't work. ## Stretch columns to fit the grid width Setting the grid's width doesn't change the width of your columns. When the columns are narrower than the grid, the space on the right stays empty. To redistribute the column widths so they fill the grid's width, use the [`stretchH`](@/api/options.md#stretchh) option: `'all'` stretches all columns proportionally, and `'last'` stretches only the last column. For live examples of both modes, see the [column stretching](@/guides/columns/column-width/column-width.md#column-stretching) section of the Column width guide. ## Autoresizing Handsontable observes window resizing. If the window's dimensions have changed, then we check if Handsontable should resize itself too. Due to the performance issue, we use the debounce method to respond on window resize. You can easily overwrite this behaviour by returning `false` in the [`beforeRefreshDimensions`](@/api/hooks.md#beforerefreshdimensions) hook. ::: only-for javascript ```js { beforeRefreshDimensions() { return false; } } ``` ::: ::: only-for react ```jsx false} /> ``` ::: ::: only-for angular ```ts import { GridSettings } from "@handsontable/angular-wrapper"; gridSettings: GridSettings = { beforeRefreshDimensions: () => false, }; ``` ```html ``` ::: ::: only-for vue ```js const hotSettings = ref({ beforeRefreshDimensions() { return false; }, }); ``` ::: ## Manual resizing The Handsontable instance exposes the [`refreshDimensions()`](@/api/core.md#refreshdimensions) method, which helps you to resize grid elements properly. ::: only-for react ::: tip To use the Handsontable API, you'll need access to the Handsontable instance. You can do that by utilizing a reference to the `HotTable` component, and reading its `hotInstance` property. For more information, see the [Instance methods](@/guides/getting-started/react-methods/react-methods.md) page. ::: ::: ::: only-for angular ::: tip To use the Handsontable API, you'll need access to the Handsontable instance. You can do that by utilizing a reference to the `HotTableComponent`, and reading its `hotInstance` property. For more information, see the [Instance access](@/guides/getting-started/angular-hot-instance/angular-hot-instance.md) page. ::: ::: ::: only-for vue ::: tip To use the Handsontable API, you'll need access to the Handsontable instance. Use a template ref on the `HotTable` component and read its `hotInstance` property. For more information, see the [Referencing the Handsontable instance in Vue 3](@/guides/getting-started/vue3-hot-reference/vue3-hot-reference.md) page. ::: ::: ```js hot.refreshDimensions(); ``` You can listen for two hooks, [`beforeRefreshDimensions`](@/api/hooks.md#beforerefreshdimensions) and [`afterRefreshDimensions`](@/api/hooks.md#afterrefreshdimensions). ::: only-for javascript ::: example #example --html 1 --css 2 --js 3 --ts 4 @[code](@/content/guides/getting-started/grid-size/javascript/example.html) @[code](@/content/guides/getting-started/grid-size/javascript/example.css) @[code](@/content/guides/getting-started/grid-size/javascript/example.js) @[code](@/content/guides/getting-started/grid-size/javascript/example.ts) ::: ::: ::: only-for react ::: example #example :react --css 1 --js 2 --ts 3 @[code](@/content/guides/getting-started/grid-size/react/example.css) @[code](@/content/guides/getting-started/grid-size/react/example.jsx) @[code](@/content/guides/getting-started/grid-size/react/example.tsx) ::: ::: ::: only-for angular ::: example #example :angular --ts 1 --html 2 @[code](@/content/guides/getting-started/grid-size/angular/example.ts) @[code](@/content/guides/getting-started/grid-size/angular/example.html) ::: ::: ::: only-for vue ::: example #example :vue --js 1 @[code](@/content/guides/getting-started/grid-size/vue/example.vue) ::: ::: ## Known limitations Handsontable relies on the browser's native scrollbars. Browsers cap how tall (or wide) a scrollable area can be, measured in CSS pixels. The taller the scroll area grows past that cap, the more rendering glitches appear - rows become misaligned, the autofill handle turns blurry, and eventually cell borders disappear. The point where these glitches start depends on the browser, the operating system, and the device. The following approximate values were measured on macOS, and mark where problems begin rather than a hard cutoff: | Browser | Glitches start around | | ------- | --------------------- | | Chrome | ~8,000,000 px | | Firefox | ~3,500,000 px | | Safari | ~16,000,000 px | These values are approximate, were measured on specific browser versions, and can change as browsers update. To estimate the maximum number of rows, divide the browser's pixel limit by your row height. With the default row height of 23 px, Chrome stays reliable up to about 350,000 rows (8,000,000 / 23). To estimate the maximum number of columns, divide the pixel limit by your column width. With a column width of 50 px, that's about 160,000 columns (8,000,000 / 50). Taller rows or wider columns lower these limits proportionally. For example, with a row height of 100 px, Chrome's limit drops to about 80,000 rows (8,000,000 / 100). If your dataset can grow past these limits, load it in smaller chunks, for example with server-side or lazy data loading. ## Result Your grid now renders at the dimensions you specified, responding to container size or fixed pixel values as configured. ## Related articles **Related guides**
- [Column widths](@/guides/columns/column-width/column-width.md) - [Row heights](@/guides/rows/row-height/row-height.md)
**Configuration options**
- [height](@/api/options.md#height) - [layoutDirection](@/api/options.md#layoutdirection) - [preventOverflow](@/api/options.md#preventoverflow) - [width](@/api/options.md#width)
**Core methods**
- [refreshDimensions()](@/api/core.md#refreshdimensions) - [updateSettings()](@/api/core.md#updatesettings)
**Hooks**
- [afterCellMetaReset](@/api/hooks.md#aftercellmetareset) - [afterRefreshDimensions](@/api/hooks.md#afterrefreshdimensions) - [afterUpdateSettings](@/api/hooks.md#afterupdatesettings) - [beforeRefreshDimensions](@/api/hooks.md#beforerefreshdimensions)
--- # Custom ID, class, and style URL: https://handsontable.com/docs/javascript-data-grid/custom-id-class-style/ Apply a custom `id`, `class`, and inline `style` to the Handsontable container, to target the grid with your own CSS or JavaScript selectors. [[toc]] ## Overview The Handsontable container is a regular DOM element, so you can give it a custom `id`, `class`, and inline `style`. How you set these attributes depends on the framework you use, but two rules apply everywhere: - `id` is not a configuration option. Handsontable does not read an `id` from the settings object passed to the constructor. The container's `id` comes from the DOM element itself, or from a prop or attribute that the framework wrapper forwards to the container. - Handsontable assigns a random `id` when the container has none. If the container element has no `id`, or an `id` that starts with `ht_`, Handsontable overwrites it with a generated value in the form `ht_`. To keep a custom `id`, use one that does not start with `ht_`. ::: only-for javascript ## Set the id, class, and style In the vanilla JavaScript build, the container is the DOM element you pass to the `Handsontable` constructor. Set its `id`, `class`, and `style` on that element before or after initialization. In the following example, the container receives a custom class and an inline border. Because the element already has the `id` `example1`, Handsontable keeps it. An element with no `id` would instead receive a generated `ht_` value. ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/getting-started/custom-id-class-style/javascript/example1.js) @[code](@/content/guides/getting-started/custom-id-class-style/javascript/example1.ts) ::: ::: ::: only-for react ## Set the id, class, and style The `HotTable` component forwards the `id`, `className`, and `style` props to the grid's container element. If you omit `id`, the wrapper assigns a generated value such as `hot-`. ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/getting-started/custom-id-class-style/react/example1.jsx) @[code](@/content/guides/getting-started/custom-id-class-style/react/example1.tsx) ::: ::: ::: only-for vue ## Set the id, class, and style The `HotTable` component forwards the `id`, `class`, and `style` attributes to the grid's container element. If you omit `id`, the wrapper assigns a generated value such as `hot-`. ::: example #example1 :vue3 @[code](@/content/guides/getting-started/custom-id-class-style/vue/example1.vue) ::: ::: ::: only-for angular ## Set the id, class, and style The Angular wrapper renders the grid into an inner container element nested inside the `` host element. The `id`, `class`, and `style` you set on `` apply to the host element, which wraps the grid. A `class` or `style` on the host therefore still surrounds the grid visually. The grid's own container element receives an automatically generated `ht_` `id`, so the `id` you set on `` does not become the container's `id`. To target the grid container directly, use a descendant selector such as `hot-table#inventory-grid > div`. ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/getting-started/custom-id-class-style/angular/example1.ts) @[code](@/content/guides/getting-started/custom-id-class-style/angular/example1.html) ::: ::: ## Result The grid container has your custom `class` and inline `style` applied, so you can target it with your own CSS or JavaScript selectors. The container's `id` is the one you set, or a generated `ht_` value when none is provided. --- # License key URL: https://handsontable.com/docs/javascript-data-grid/license-key/ Activate Handsontable, passing your license key in the configuration object. Use a special key for non-commercial and evaluation purposes. [[toc]] ## Overview Handsontable is available under Commercial and Free licenses, depending on your usage. We require you to specify which terms apply to your usage, by passing a license key in Handsontable's [`licenseKey`](@/api/options.md#licensekey) configuration option. ## Commercial license If you use the paid version of Handsontable, pass the string of numbers delivered to you after the purchase. Note that the license key is a string, so you need to wrap it in quotes `''`. ::: only-for javascript ```js const settings = { licenseKey: '00000-00000-00000-00000-00000', //... other options } ``` To use it with a framework, pass the string to a [`licenseKey`](@/api/options.md#licensekey) prop: ```jsx ``` ```html ``` ```html ``` ```html ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular Pass the license key through `GridSettings` in your component: ```ts import { GridSettings } from "@handsontable/angular-wrapper"; readonly gridSettings: GridSettings = { licenseKey: '00000-00000-00000-00000-00000', }; ``` ```html ``` Or set it globally for all tables via `HOT_GLOBAL_CONFIG` in `app.config.ts`: ```ts import { ApplicationConfig, provideZoneChangeDetection } from "@angular/core"; import { HOT_GLOBAL_CONFIG, HotGlobalConfig } from "@handsontable/angular-wrapper"; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), { provide: HOT_GLOBAL_CONFIG, useValue: { license: '00000-00000-00000-00000-00000' } as HotGlobalConfig, }, ], }; ``` ::: ::: only-for vue Pass the license key as a prop: ```html ``` Or include it in the settings object passed to `:settings`: ```vue ``` ::: ## Non-commercial license If you use Handsontable for purposes not intended toward monetary compensation such as, but not limited to, teaching, academic research, evaluation, testing and experimentation, pass the string `'non-commercial-and-evaluation'`. With this key, the grid displays no notification of any kind, and the console stays silent, because the Non-Commercial and Evaluation License permits your usage. ::: only-for javascript ```js const settings = { licenseKey: 'non-commercial-and-evaluation', //... other options } ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular Pass the license key through `GridSettings` in your component: ```ts import { GridSettings, NON_COMMERCIAL_LICENSE, } from "@handsontable/angular-wrapper"; readonly gridSettings: GridSettings = { licenseKey: NON_COMMERCIAL_LICENSE, }; ``` ```html ``` Or set it globally for all tables via `HOT_GLOBAL_CONFIG` in `app.config.ts`: ```ts import { ApplicationConfig, provideZoneChangeDetection } from "@angular/core"; import { HOT_GLOBAL_CONFIG, HotGlobalConfig, NON_COMMERCIAL_LICENSE, } from "@handsontable/angular-wrapper"; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), { provide: HOT_GLOBAL_CONFIG, useValue: { license: NON_COMMERCIAL_LICENSE } as HotGlobalConfig, }, ], }; ``` ::: ::: only-for vue ```html ``` ::: ## Entitlement license keys Handsontable also accepts entitlement license keys. An entitlement key is plain-English text that states what you licensed, followed by a bracketed block that the grid reads: ```text This is a Handsontable license key for Acme Corp, issued on 2026-08-12 for the "Acme Portal" project. It includes 1 license: > 1. Subscription license under Handsontable Subscription License Agreement 2.0 of 2022-05-21, for Handsontable on the Enterprise package, for internal use, valid until 2027-08-12 (UTC). Use after that date is not permitted. To renew, contact sales@handsontable.com. [eyJwcm9kdWN0cyI6eyJoYW5kc29udGFibGUi...3a4f8361] ``` Pass the whole key string, exactly as you received it, in the same [`licenseKey`](@/api/options.md#licensekey) option. The key text contains double quotes (around the project name) and can contain apostrophes (in a company name), so do not paste it into a quoted string or a quoted HTML attribute - the first quote inside the key ends the value there. Read the key from your configuration, or write it as a template literal (in backticks), as in the examples below. A template literal works as long as the key text contains no backtick and no `${`; if it does, read the key from your configuration. ::: only-for javascript ```js const settings = { licenseKey: `This is a Handsontable license key for Acme Corp, issued on 2026-08-12 for the "Acme Portal" project. ... [eyJwcm9kdWN0cyI6...3a4f8361]`, //... other options } ``` ::: ::: only-for react ```jsx const licenseKey = `This is a Handsontable license key for Acme Corp, issued on 2026-08-12 for the "Acme Portal" project. ... [eyJwcm9kdWN0cyI6...3a4f8361]`; ``` ::: ::: only-for angular ```ts import { GridSettings } from "@handsontable/angular-wrapper"; readonly gridSettings: GridSettings = { licenseKey: `This is a Handsontable license key for Acme Corp, issued on 2026-08-12 for the "Acme Portal" project. ... [eyJwcm9kdWN0cyI6...3a4f8361]`, }; ``` ::: ::: only-for vue ```html ``` ::: Keys issued in the 25-character format keep working without any change. Pass the key unchanged. A newer key protects its text as well as the bracketed block, so editing the text, removing it, pasting the bracketed block alone, or adding text after the block makes it invalid. Keys issued earlier keep working the way they did in Handsontable 18.1. Spaces and line breaks are ignored, in the text and inside the block, so you can rewrap the key, paste it out of an email that broke the block across lines, or put the whole key on one line, and the key still works. Line breaks saved as the characters `\n`, as some `.env` files and CI secret fields store them, work too. End the key with the block - only whitespace, or line breaks saved as `\n`, may follow it. Space and line breaks around the whole key are trimmed for you, so a key pasted with a trailing newline still works. Handsontable 18.1 does not ignore line breaks inside the block. If you use 18.1, make sure the block is on one line, with no spaces in it. Later versions check newer keys more strictly than 18.1, so before you upgrade from 18.1, make sure you pass the whole key, exactly as you received it - not only the bracketed block, and with nothing after it. When you store the key in an environment variable, a `.env` file, or a CI secret, put it on one line. In a `.env` file, also wrap the key in single quotes (`'...'`), or in backticks if the key text contains an apostrophe. Without quotes, a line break or a ` #` in the key cuts the value short, and double quotes end at the first double quote inside the key. Do not escape the quotes inside the key (`\"`) - the backslashes stay in the value, and the key becomes invalid. Never change any other character of the key, for example by replacing straight quotes with curly ones. Each license behaves differently around its date: ### Trial keys A trial key is time-boxed. During the trial, the grid shows the Handsontable badge in its top-left corner. Hover over the badge to see the trial status. Once the key's notice period begins, the console starts warning how many days remain - on a standard 45-day trial that is from day one. On the last licensed day the warning says the key expires today, because that day is still licensed in full. After the expiration date, a message opens next to the badge and below the grid, and the console reports an error. When the grace period stored in the key also passes, a blocking screen replaces the grid. To purchase a commercial license, contact our [Sales Team](https://handsontable.com/get-a-quote). ### Subscription keys The console warns you before the expiration date. How early depends on the notice period stored in your key. After the expiration date, the console reports an error. The grid itself displays no message, and it never stops working. To renew your subscription, contact our [Sales Team](https://handsontable.com/get-a-quote). The expiration date is a full calendar day in UTC. Your license runs to the end of the named day, wherever you are. ### Perpetual keys A perpetual key works like the 25-character commercial keys: its maintenance date is compared against the build date of your Handsontable version, never against the current date. You can use the versions released on or before that date indefinitely, including offline. On a newer version, the console reports an error and the grid shows a notice below the table. ## Result Your grid is now licensed. A valid commercial key removes the license notice from the grid header. ## The validation process We validate the license key to determine whether you are entitled to use the software. To do that, we compare the time between two dates. These dates come from two sources of information. One is the `build date` that is provided in each version of Handsontable. The other is the `creation date` that comes with the license key. This process does not trigger any connection to any server. Entitlement keys extend this process. A trial or subscription key carries its expiration date inside the key, and Handsontable compares that date against the current date, in UTC. A perpetual key keeps the build-date comparison described above. No key triggers any connection to any server. ## Notifications If your license key is missing, invalid, or expired, Handsontable displays a notification. Where it appears depends on the problem. A **missing or invalid key** blocks the grid. Handsontable covers it with a modal that cannot be closed, and repeats the message in the console. Create the instance with a valid key to remove it. The grid is not usable until you do. ![The blocking modal for a missing license key, light theme](/img/pages/license-key/missing-license-key-light.png) ![The blocking modal for a missing license key, dark theme](/img/pages/license-key/missing-license-key-dark.png) The modal covers only the grid. The rest of your page keeps working. The modal does not appear on this site, so this page shows it as an image. An **expired key** does not block anything. Its message appears below the table and in the console, and every feature keeps working. The messages are as follows: ### Missing license key Shown in a blocking modal and in the console: _The license key for Handsontable is missing. Use your purchased key to activate the product. Alternatively, you can activate Handsontable to use for non-commercial purposes by passing the key: ‘non-commercial-and-evaluation’. Read more about it in the documentation or contact us at `[email]`._ ### Invalid license key Shown in a blocking modal and in the console: _The license key for Handsontable is invalid. Read more on how to install it properly or contact us at `[email]`._ ### Expired license key Shown below the table and in the console: _The license key for Handsontable expired on `[expiration_date]`, and is not valid for the installed version `[handsontable_version]`. Renew your license key or downgrade to a version released prior to `[expiration_dates]`. If you need any help, contact us at `[email]`._ ## Get a license key To get a commercial license key for your Handsontable copy, contact our [Sales Team](https://handsontable.com/get-a-quote). ## FAQ ### How do I update the license key in many tables at once? There's no built-in method to update all existing instances automatically. You have two options: - **Before creating any instances:** set the key globally so every new instance picks it up. ```js Handsontable.defaults.licenseKey = 'your-new-key'; ``` - **For instances that already exist:** Handsontable reads the license key once, when it creates an instance. [`updateSettings()`](@/api/core.md#updatesettings) does not apply a new key to a running grid. Destroy each instance and create it again with the new key, looping through them manually. Re-creating an instance resets its UI state, such as sorting, filters, selection, and undo history. ::: only-for javascript To re-create an instance, destroy it and construct it again with the new key: ```js hot1.destroy(); hot1 = new Handsontable(container1, { ...settings1, licenseKey: 'your-new-key' }); hot2.destroy(); hot2 = new Handsontable(container2, { ...settings2, licenseKey: 'your-new-key' }); ``` ::: ::: only-for react angular vue The wrapper component creates its instance once, when it mounts. A later change to its `licenseKey` goes through `updateSettings()`, so it has no effect. Remount the component instead, for example by changing its `key` in React, toggling `*ngIf` in Angular, or toggling `v-if` in Vue. ::: ### What happens if I keep using a stale (expired) license key? What you can do depends on your license type: - **Subscription license:** once your subscription expires, you can no longer use Handsontable. Renew your subscription to keep using the grid. - **Perpetual license:** you can keep using any version of Handsontable that was released _before_ your license expired. You won't be able to upgrade to versions released after that date. If you load a version that your license doesn't cover, you'll see a [console warning](#expired-license-key) and a watermark on the grid. To keep support and access to updates, keep your license up to date. ### What will I see if I update to a newer version with a stale key? If your license key is expired and you update to a version of Handsontable released after it expired, you'll see: - A watermark below the table saying the license is invalid. - A warning in the browser console (see [Expired license key](#expired-license-key)). All features still work, but you're not entitled to use that version under an expired license. To remove the warning and watermark, either downgrade to the last version released while your license was still active, or renew the license. ## Related articles ### Related guides
- [Software license](@/guides/technical-specification/software-license/software-license.md)
### Related API reference
- Configuration options: - [`licenseKey`](@/api/options.md#licensekey)
--- # Instance methods URL: https://handsontable.com/docs/react-data-grid/instance-methods/ Use useRef and HotTableRef to get a reference to the Handsontable instance from a React component, then call any API method on it. [[toc]] ## Use Handsontable's API You can programmatically change the internal state of Handsontable beyond what's possible with props. To do that, call API methods of the relevant Handsontable instance associated with your instance of the [`HotTable`](@/guides/getting-started/installation/installation.md#_4-use-the-hottable-component) component. The following example implements the [`HotTable`](@/guides/getting-started/installation/installation.md#_4-use-the-hottable-component) component showing how to reference the Handsontable instance from the wrapper component. ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/getting-started/react-methods/react/example1.jsx) @[code](@/content/guides/getting-started/react-methods/react/example1.tsx) ::: ::: tip Mobile and Android support Handsontable is designed for desktop browsers. While the library may load on mobile browsers (including Android), features such as cell editing, context menus, and keyboard navigation may not work as expected on touch devices. ::: ## TypeScript: type the `hotInstance` reference Since 14.1.0, `HotTable` is a functional component, not a class component. Because of this, you can no longer use `HotTable` as the type argument of `useRef` to get access to the Handsontable instance: ```typescript // This no longer works as expected since 14.1.0 import HotTable from '@handsontable/react-wrapper'; const ref = useRef(null); // TypeScript error or wrong type ``` Instead, use the `HotTableRef` interface exported from `@handsontable/react-wrapper`: ```typescript import { HotTable, HotTableRef } from '@handsontable/react-wrapper'; const ref = useRef(null); // Access the Handsontable instance through `hotInstance`: ref.current?.hotInstance?.selectCell(1, 1); ``` `HotTableRef` exposes the following properties: | Property | Type | Description | |---|---|---| | `hotInstance` | `Handsontable \| null` | The Handsontable instance. Use this to call Handsontable API methods. | | `hotElementRef` | `HTMLElement` | The root DOM element of the grid. | ## Result Your React component now holds a typed reference to the Handsontable instance through `HotTableRef`. You can call any Handsontable API method on `ref.current.hotInstance` from event handlers and lifecycle effects. --- # Integration with Redux URL: https://handsontable.com/docs/react-data-grid/redux/ In this tutorial, you will connect a Handsontable grid to a Redux store. You will learn to dispatch actions on cell changes and sync grid data with global state. [[toc]] ## Integrate with Redux ::: tip Before using any state management library, make sure you know how Handsontable handles data: see the [Binding to data](@/guides/getting-started/binding-to-data/binding-to-data.md#understand-binding-as-a-reference) page. ::: The following example implements the `@handsontable/react-wrapper` component with a [`readOnly`](@/api/options.md#readonly) toggle switch and the Redux state manager. ## Simple example ::: example #example1 :react-redux --js 1 --ts 2 --deps redux@4 react-redux@7.2.4 @[code](@/content/guides/getting-started/react-redux/react/example1.jsx) @[code](@/content/guides/getting-started/react-redux/react/example1.tsx) ::: ## Advanced example This example shows: - A [custom editor](@/guides/cell-functions/cell-editor/cell-editor.md#component-based-editors) component (built with an external dependency, `HexColorPicker`). This component acts both as an editor and as a renderer. - A [custom renderer](@/guides/cell-functions/cell-renderer/cell-renderer.md#declare-a-custom-renderer-as-a-component) component, built with a local `StarRating` component. The editor component changes the behavior of the renderer component, by passing information through Redux (and the `connect()` method of `react-redux`). ::: example #example6 :react-advanced --js 1 --ts 2 --deps redux@4 react-redux@7.2.4 react-colorful@5.5.1 @[code](@/content/guides/getting-started/react-redux/react/example6.jsx) @[code](@/content/guides/getting-started/react-redux/react/example6.tsx) ::: ## What you learned - You can connect a `HotTable` component to a Redux store using `react-redux`'s `connect()` method. - The `afterChange` hook dispatches Redux actions whenever the user edits a cell, keeping global state in sync. - Custom editor and renderer components can read from and write to the Redux store, enabling grid cells to reflect shared application state. ## Next steps - [Binding to data](@/guides/getting-started/binding-to-data/binding-to-data.md) -- understand how Handsontable binds to different data structures. - [Saving data](@/guides/getting-started/saving-data/saving-data.md) -- learn additional patterns for persisting grid changes. --- # Instance access URL: https://handsontable.com/docs/angular-data-grid/instance-access/ Use @ViewChild to get a reference to the Handsontable instance from your Angular component, then call API methods programmatically. [[toc]] ## Use Handsontable's API You can programmatically change the internal state of Handsontable beyond what's possible with props. To do this, call the API methods of the relevant Handsontable instance associated with your instance of the [`HotTableComponent`](@/guides/getting-started/installation/installation.md#use-the-hottable-component). Access to the Handsontable instance can be obtained as early as in the `ngAfterViewInit()` lifecycle hook by using the `@ViewChild` decorator, which holds a reference to the wrapper component. The following example implements the [`HotTableComponent`](@/guides/getting-started/installation/installation.md#use-the-hottable-component) showing how to reference the Handsontable instance from the wrapper component. ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/getting-started/angular-hot-instance/angular/example1.ts) @[code](@/content/guides/getting-started/angular-hot-instance/angular/example1.html) ::: ## Result Your Angular component now holds a reference to the Handsontable instance through `@ViewChild`. You can call any Handsontable API method on that instance from within the component's lifecycle hooks or event handlers. --- # Instance reference URL: https://handsontable.com/docs/vue-data-grid/vue-instance-reference/ Use `useTemplateRef` and the `hotInstance` property to get a reference to the Handsontable instance from a Vue 3 component, then call any API method on it. [[toc]] ## Use Handsontable's API You can programmatically change the internal state of Handsontable beyond what's possible with props. To do that, call API methods of the relevant Handsontable instance associated with your instance of the [`HotTable`](@/guides/getting-started/installation/installation.md#use-the-hottable-component) component. In ` ``` The string passed to `useTemplateRef()` must match the `ref` attribute on `HotTable`. Access the grid through `hotTableRef.value?.hotInstance`. The following live example selects cell B2 when you click the button. ::: example #example1 :vue3 @[code](@/content/guides/getting-started/vue3-hot-reference/vue/example1.vue) ::: ## Vue versions before 3.5 `useTemplateRef` requires Vue 3.5 or newer. On earlier versions, use the `ref` function instead. Declare a `ref` initialized to `null`, then give the `HotTable` component a `ref` attribute whose value matches the variable name. ```vue ``` ## Result Your Vue 3 component now holds a reference to the Handsontable instance. You can call any Handsontable API method on that instance -- such as `selectCell()` or `getPlugin()` -- from event handlers or lifecycle hooks. --- # HotColumn component URL: https://handsontable.com/docs/vue-data-grid/vue-hot-column/ HotColumn is a Vue 3 component that lets you define column settings declaratively as child components of HotTable. [[toc]] [Find out which Vue 3 versions are supported](@/guides/getting-started/installation/installation.md#supported-versions-of-vue) ## Declare column settings To declare column-specific settings, pass the settings as `` properties, either separately or wrapped as a `settings` property, exactly as you would for ``. ::: example #example1 :vue3 @[code](@/content/guides/getting-started/vue3-hot-column/vue/example1.vue) ::: ## Array of objects To work with an array of objects for the `` component, you need to provide precise information about the data structure for the columns. To do this, refer to the data for a column in properties as `data`. ::: example #example2 :vue3 @[code](@/content/guides/getting-started/vue3-hot-column/vue/example2.vue) ::: ## Declare a custom editor as a component You can declare a custom editor by creating a class that extends `TextEditor` and passing it to a `` via the `editor` prop. The editor's input element uses a `placeholder` attribute to display a hint when the cell value is empty. ::: example #example3 :vue3 @[code](@/content/guides/getting-started/vue3-hot-column/vue/example3.vue) ::: ## Declare a custom renderer You can declare a custom renderer by creating a function that matches the [`BaseRenderer`](@/api/renderers.md) signature and passing it to a `` via the `renderer` prop. The renderer receives the cell's `td` element and the cell value, and must return the modified `td`. ::: example #example4 :vue3 @[code](@/content/guides/getting-started/vue3-hot-column/vue/example4.vue) ::: ## Render columns dynamically You can render `` components with `v-for` and drive them from a reactive array. When you add, remove, or reorder entries in that array, the grid updates its columns to match. Use a stable `key` (for example, the column's `data` property) so reordering preserves each column's settings. ::: example #example5 :vue3 @[code](@/content/guides/getting-started/vue3-hot-column/vue/example5.vue) ::: Dynamic rendering also works when a `` is nested inside your own wrapper component. Custom renderers and editors are passed as functions through the `renderer` and `editor` props, as shown above. The Vue 3 wrapper does not render cells from Vue components. ## Result Using `HotColumn` child components, each column reads its settings declaratively from Vue props rather than from a flat `columns` array, keeping your template in sync with your column configuration. --- # Vuex state management URL: https://handsontable.com/docs/vue-data-grid/vue-vuex/ In this tutorial, you will connect a Handsontable grid to a Vuex store. You will learn to commit mutations on cell changes and sync grid data with shared application state. [[toc]] ## Before you begin - Install [Vuex 4](https://vuex.vuejs.org/) in your Vue 3 project: `npm install vuex@next`. - Understand how Handsontable handles data: see [Binding to data](@/guides/getting-started/binding-to-data/binding-to-data.md#understand-binding-as-a-reference). ## Integrate with Vuex ::: tip Before using any state management library, make sure you know how Handsontable handles data: see the [Binding to data](@/guides/getting-started/binding-to-data/binding-to-data.md#understand-binding-as-a-reference) page. ::: The following example implements the `@handsontable/vue3` component with a [`readOnly`](@/api/options.md#readonly) toggle switch and the Vuex state manager. Editing a cell commits a mutation to the store, and a live store dump displays the current state below the grid. [Find out which Vue 3 versions are supported](@/guides/getting-started/installation/installation.md#supported-versions-of-vue) ::: example #example1 :vue3-vuex @[code](@/content/guides/getting-started/vue3-vuex/vue/example1.vue) ::: ## What you learned - How to create a typed Vuex store with `createStore` and define mutations for grid data and settings. - How to use the `afterChange` hook to commit a Vuex mutation whenever the user edits a cell. - How to toggle a Handsontable option (`readOnly`) by dispatching a store mutation from a UI control. - How to subscribe to store changes with `store.subscribe()` to reactively update a live state dump. ## Next steps - [Pinia state management](@/guides/getting-started/vue3-pinia/vue3-pinia.md) -- use Pinia as a lighter, Vue 3-native alternative to Vuex for managing grid state. - [Referencing the Handsontable instance in Vue 3](@/guides/getting-started/vue3-hot-reference/vue3-hot-reference.md) -- access the Handsontable API directly from your component. - [HotColumn component in Vue 3](@/guides/getting-started/vue3-hot-column/vue3-hot-column.md) -- configure columns declaratively alongside your store. --- # Pinia state management URL: https://handsontable.com/docs/vue-data-grid/vue-pinia/ Use [Pinia](https://pinia.vuejs.org/) -- the officially recommended state-management library for Vue 3 -- to manage your Handsontable data and settings. [[toc]] ## Prerequisites - The `@handsontable/vue3` package installed. See [Installing Handsontable](@/guides/getting-started/installation/installation.md). - The `pinia` package installed: `npm install pinia`. ## Steps ### 1. Define a Pinia store Create a store with `defineStore`. The example below defines state that holds the grid data and a `readOnly` flag, plus actions to mutate both. ### 2. Activate Pinia Because the docs example runner does not call `app.use(pinia)`, activate Pinia at module scope with `setActivePinia(createPinia())` before calling `useStore()`. ### 3. Connect the store to `` Bind the store state to your `gridSettings` ref and use the `afterChange` hook to write cell edits back to the store. ### 4. React to store changes from the grid Use Vue's `watch` to observe store state and update the grid when the store changes outside of a direct cell edit -- for example, when a button dispatches an action. ## Example ::: example #example1 :vue3 @[code](@/content/guides/getting-started/vue3-pinia/vue/example1.vue) ::: ## Result The grid reflects the Pinia store state at all times. Cell edits update the store, and store mutations (for example, toggling `readOnly` or resetting data) are immediately reflected in the grid. ## Related - [Pinia documentation](https://pinia.vuejs.org/) - [Vuex in Vue 3](@/guides/getting-started/vue3-vuex/vue3-vuex.md) -- an alternative state management approach using Vuex 4. - [Instance access in Vue 3](@/guides/getting-started/vue3-hot-reference/vue3-hot-reference.md) -- access the Handsontable API directly from your component. --- # Use Handsontable in Nuxt URL: https://handsontable.com/docs/vue-data-grid/vue-nuxt/ Handsontable relies on browser APIs that are not available during server-side rendering. This page explains how to integrate Handsontable into a Nuxt 3 application without SSR errors. [[toc]] ## Why Handsontable needs client-side rendering Nuxt 3 renders Vue components on the server before sending HTML to the browser. That server environment has no DOM -- no `window`, no `document`, no `HTMLElement`. Handsontable's initialization code and the underlying Walkontable rendering engine access these browser globals. When Nuxt runs a component that imports or initializes Handsontable on the server, you get errors like: ``` ReferenceError: window is not defined ``` The fix is to keep all Handsontable code out of the server rendering path. ## Prerequisites - Nuxt 3 project set up and running. - Handsontable and the Vue 3 wrapper installed: ```bash npm install handsontable @handsontable/vue3 ``` ## Using `` Nuxt's built-in `` component prevents its children from rendering on the server. Wrap your `` in it: ```vue ``` The `#fallback` slot renders on the server in place of the grid. Use it to show a placeholder or skeleton so the page has visible content before the browser takes over. ::: tip `` prevents **rendering** on the server but the `import` statements still execute on the server. If you see a `window is not defined` error at import time rather than render time, use the `.client.vue` approach described below. ::: ## Using the `.client.vue` suffix Nuxt treats any component file ending in `.client.vue` as a client-only component -- it is excluded from the server bundle entirely, including its imports. This is the most robust option when a package accesses browser APIs at module load time. Create a wrapper component named with the `.client.vue` suffix: ``` components/ └── HandsonGrid.client.vue ``` ```vue ``` Use it in your page without any additional wrapper: ```vue ``` ::: tip The `.client.vue` convention only works with Nuxt auto-imports and `#components` imports. If you import the component using its full file path (for example, `import HandsonGrid from '~/components/HandsonGrid.client.vue'`), Nuxt does not apply the client-only behavior. ::: ## `nuxt.config.ts` notes For most Nuxt 3 projects (which use Vite by default), no extra configuration is needed. `` or `.client.vue` is sufficient. In rare cases -- for example, when using a Nuxt plugin that imports Handsontable in a server-side context -- you can add the packages to `build.transpile` as a fallback: ```typescript // nuxt.config.ts export default defineNuxtConfig({ build: { transpile: ['handsontable', '@handsontable/vue3'], }, }); ``` Start with `` or `.client.vue` before reaching for this option. ## Result Handsontable renders in the browser. The SSR pass shows the `#fallback` content (with ``) or nothing (with `.client.vue`), and the grid initializes once the Vue component mounts on the client.
- [Working example with Nuxt](https://handsontable.com/codesandbox-vm?example-dir=nuxt&handsontable-version=18.1.2)
## Related - [Vue 3 installation](@/guides/getting-started/installation/installation.md) --- # Skills for Claude Code URL: https://handsontable.com/docs/javascript-data-grid/skills-for-claude-code/ Skills for Claude Code are bundled instructions that give Claude deep knowledge of Handsontable and HyperFormula. Install them once, then ask Claude to build, configure, or debug -- it pulls from the same product docs you're reading right now, so the code it writes matches current APIs instead of guessing from outdated training data. ## Two skills, one install The repo ships two skills: - **handsontable** -- the data grid component. Covers React, Angular, Vue, and vanilla JS setup, configuration, theming, cell types, sorting, filtering, formulas, hooks, performance, and version migration. - **hyperformula** -- the headless calculation engine. Covers instance creation, CRUD, custom functions, named expressions, error handling, batch operations, and Node.js usage. Use **handsontable** when you're building a visible grid in a web app. Use **hyperformula** when you're evaluating formulas programmatically without a UI -- server-side calculations, pricing engines, what-if analysis. Claude loads whichever is relevant based on what you ask. ## Install with one command In Claude Code, add the marketplace and install both skills: ```bash /plugin marketplace add handsontable/handsontable-skills /plugin install handsontable-skills@handsontable-skills ``` For Cowork or Claude.ai web, download the zip from the latest GitHub release and drag it into chat. For the Claude API, upload the skill folder directly. Full instructions live in the repo README. ## Versioned to match releases Each skill is tagged to the product version it targets -- `handsontable/v18.1.2` is the skill for Handsontable 18.1.2, and each HyperFormula release gets a matching `hyperformula/v*` tag. You always know which API surface Claude is working from. Browse the Skills for Claude Code --- # AI Theme Builder URL: https://handsontable.com/docs/javascript-data-grid/ai-theme-builder/ The Theme Builder is a free web tool for building Handsontable themes visually, instead of writing code. You can manually adjust over 200 design tokens with UI controls and see the table update in real-time. Then you can export the theme and use it in your own app! To go deeper on the underlying theme system, see the [Themes](@/guides/styling/themes/themes.md) and [Theme customization](@/guides/styling/theme-customization/theme-customization.md) guides. ## AI theme generation Click the AI tab on the right to generate themes from a prompt. Describe the look you want -- for example, "minimal light theme with rounded headers" or "dark theme with high-contrast borders" -- and the builder generates the theme configuration file. You can ask for follow up changes, or switch over to the manual editor to make any final adjustments before exporting. ## Multi-framework support The Theme Builder generates working configurations for vanilla JavaScript, React, and Angular. So it's easy to use the generated theme in your own framework. Click the **Generate code** button, then choose your framework, and copy the theme. ## Test in StackBlitz Try out your theme in a live web application by clicking the **Open in StackBlitz** button. This opens a new tab in StackBlitz where you can test and edit the theme in the web instead of downloading the files to run locally. Open the Theme Builder --- # AI Docs Assistant URL: https://handsontable.com/docs/javascript-data-grid/ai-docs-assistant/ The AI Docs Assistant is the **Ask AI** button in the docs header. It answers questions about Handsontable and HyperFormula, writes code examples, and links you to the relevant guide or API reference page. ::: tip **How can I help?** I search the docs to answer questions about APIs, configuration, and usage. I say "I don't know" when the docs don't cover it. ::: The main search bar (Cmd + K) searches using keyword matches, but the Docs Assistant uses semantic search to match on concepts and related topics. Use the Docs Assistant when you want to chat about a topic or see code examples generated. Click the **Ask AI** button in the header to get started. ## International language support Ask your question in any language and the assistant will reply accordingly, while keeping API methods, function names, and other key terms in English to ensure valid code generation. ## Current page context Ask questions about the current page without copy/pasting the contents. The Docs Assistant knows what page you're on, and you can even switch pages and continue the conversation. --- # Docs MCP Server URL: https://handsontable.com/docs/javascript-data-grid/docs-mcp-server/ The Docs MCP Server is a public Model Context Protocol endpoint that gives any MCP-capable AI agent live semantic search over the Handsontable and HyperFormula knowledge base: ``` https://docs-assistant.handsontable.com/mcp ``` It runs on the same retrieval that powers the [AI Docs Assistant](@/guides/ai-tools/ai-docs-assistant/ai-docs-assistant.md), exposed as a tool your agent calls while it works. No install, no authentication, no API key. ## What your agent can search The knowledge base covers more than the docs site: - Documentation guides and the API reference - HyperFormula guides - Code recipes distilled from the [Skills for Claude Code](@/guides/ai-tools/skills-for-claude-code/skills-for-claude-code.md) - Release notes for both products - GitHub issues for both products, including resolved discussions - Blog posts The index always tracks the latest release of each product, so your agent retrieves current APIs instead of guessing from outdated training data. ## Connect your agent ### Claude Code ```bash claude mcp add --transport http handsontable-docs https://docs-assistant.handsontable.com/mcp ``` If you use Claude Code, also install the [Skills for Claude Code](@/guides/ai-tools/skills-for-claude-code/skills-for-claude-code.md) -- the skills teach Claude how to work with Handsontable, and the MCP server backs that up with live retrieval. ### Cursor Add the server to `.cursor/mcp.json`: ```json { "mcpServers": { "handsontable-docs": { "url": "https://docs-assistant.handsontable.com/mcp" } } } ``` ### Other MCP clients The server uses the streamable HTTP transport, so any MCP client connects with the URL alone. Add `https://docs-assistant.handsontable.com/mcp` wherever your client configures remote MCP servers. ## The search_docs tool The server exposes one tool: `search_docs`. | Argument | Type | Description | |---|---|---| | `query` | string, required | What to search for, in natural language or keywords. | | `limit` | number, optional | Maximum number of snippets to return. | | `ht_version` | string, optional | Expected Handsontable version. The server validates it against the knowledge base coverage. | | `hf_version` | string, optional | Expected HyperFormula version. Validated the same way. | Results are ranked page-level snippets. Each snippet carries a `url` when a public page exists for it, and the response reports which product versions the knowledge base covers. ## Which AI tool should you use? - **[AI Docs Assistant](@/guides/ai-tools/ai-docs-assistant/ai-docs-assistant.md)** -- you're reading the docs and want answers in chat form. - **[Skills for Claude Code](@/guides/ai-tools/skills-for-claude-code/skills-for-claude-code.md)** -- you code with Claude Code, Codex, or another agent that supports skills. - **Docs MCP Server** -- any other MCP-capable agent, or any workflow where the agent should look up current documentation at run time. They combine well: skills give the agent working knowledge up front, and the MCP server answers the questions that come up along the way. ::: tip The Docs MCP Server searches documentation. If you want your agent to run spreadsheet calculations through HyperFormula instead, that's the separate HyperFormula MCP Server (early access). ::: --- # Themes URL: https://handsontable.com/docs/javascript-data-grid/themes/ Added in Handsontable 15.0.0. Use Handsontable's built-in themes or customize its look using the Theme API or CSS variables. [[toc]] ## Overview Handsontable themes manage most visual elements of the data grid. Three built-in themes are available: `main`, `horizon`, and `classic`. All themes include dark and light modes. The recommended way to apply themes is using the **Theme API**, which allows you to register and configure themes programmatically with runtime features like density modes and color schemes. Alternatively, you can use **CSS files** and pass the theme name as a string for a simpler setup. ## Built-in themes The `main` theme offers a spreadsheet-like interface, perfect for batch-editing tasks and providing users with a familiar experience, similar to other popular spreadsheet software on the market. The `horizon` theme, on the other hand, is better suited for data display and analysis. It hides the vertical lines between columns, giving it a cleaner and more lightweight feel. The `classic` theme is a replacement for the old legacy style. It retains the familiar look and feel of the original legacy styles, but has been updated to allow customization with CSS variables. This theme is ideal for users who prefer the traditional appearance of Handsontable but want to benefit from the theming system. The `classic` theme supports both light and dark modes, ensuring a seamless integration with your application's color scheme preferences. Keep in mind that starting from version `15.0`, importing a theme is required. ::: tip Default theme 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. ::: ::: only-for javascript ::: example #exampleTheme --html 1 --js 2 --ts 3 --css 4 @[code](@/content/guides/styling/themes/javascript/exampleTheme.html) @[code collapse={9-110}](@/content/guides/styling/themes/javascript/exampleTheme.js) @[code collapse={9-111}](@/content/guides/styling/themes/javascript/exampleTheme.ts) @[code](@/content/guides/styling/themes/javascript/exampleTheme.css) ::: ::: ::: only-for react ::: example #exampleTheme .disable-auto-theme :react --js 1 --ts 2 --css 3 @[code collapse={13-114}](@/content/guides/styling/themes/react/exampleTheme.jsx) @[code collapse={14-115}](@/content/guides/styling/themes/react/exampleTheme.tsx) @[code](@/content/guides/styling/themes/react/exampleTheme.css) ::: ::: ::: only-for angular ::: example #example1 :angular --css 1 --ts 2 --html 3 @[code](@/content/guides/styling/themes/angular/exampleTheme.css) @[code](@/content/guides/styling/themes/angular/example1.ts) @[code](@/content/guides/styling/themes/angular/example1.html) ::: ::: ::: only-for vue ::: example #exampleTheme .disable-auto-theme :vue3 --css 1 @[code](@/content/guides/styling/themes/vue/exampleTheme.css) @[code collapse={14-116,221-233}](@/content/guides/styling/themes/vue/exampleTheme.vue) ::: ::: ## Light and dark modes Each theme comes with three modes: - Light mode - Dark mode - Auto mode When using the Theme API, you can configure the color scheme using `setColorScheme()` with `'light'`, `'dark'`, or `'auto'` values. The `'auto'` option allows programmatic control over light/dark switching based on your application's logic. When using CSS files, color scheme switching is controlled through CSS class names. Use `ht-theme-{name}` for light mode, `ht-theme-{name}-dark` for dark mode, or `ht-theme-{name}-dark-auto` for automatic switching based on system preferences (e.g., `ht-theme-main`, `ht-theme-main-dark`, `ht-theme-main-dark-auto`). If you don't want to declare a theme at all, set the [`colorScheme`](@/api/options.md#colorscheme) option instead. See [Set the color scheme or density without a theme](#set-the-color-scheme-or-density-without-a-theme). ## Use a theme There are two ways to apply a theme. The recommended approach is to use the Theme API with a theme object, which provides full access to runtime configuration features like density modes and color schemes. ### Option 1: Using the Theme API (recommended) The Theme API allows you to import and register themes programmatically. This approach provides runtime access to theme customization features. #### Step 1. Import a theme ::: only-for javascript ```js import Handsontable from 'handsontable'; import { classicTheme } from 'handsontable/themes'; const hot = new Handsontable(container, { theme: classicTheme, // ... other options }); ``` ::: ::: only-for react ```jsx import { HotTable } from '@handsontable/react-wrapper'; import { mainTheme, registerTheme } from 'handsontable/themes'; function App() { return ( ); } ``` ::: ::: only-for angular ```ts import { mainTheme, registerTheme } from 'handsontable/themes'; // In your component @Component({ template: `` }) export class AppComponent { hotSettings = { theme: mainTheme, // ... other options }; } ``` ::: ::: only-for vue ```ts import { ref } from 'vue'; import { HotTable } from '@handsontable/vue3'; import { registerAllModules } from 'handsontable/registry'; import { mainTheme } from 'handsontable/themes'; registerAllModules(); const hotSettings = ref({ theme: mainTheme, // ... other options licenseKey: 'non-commercial-and-evaluation', }); ``` ```html ``` ::: #### Step 2. Configure the theme You can configure the theme before creating the instance using the builder pattern: ::: only-for javascript ```js import { mainTheme, registerTheme } from 'handsontable/themes'; const theme = registerTheme(mainTheme) .setColorScheme('auto') // 'light', 'dark', or 'auto' .setDensityType('comfortable'); // 'default', 'compact', or 'comfortable' const hot = new Handsontable(container, { theme, }); ``` Or configure it after init by retrieving the registered theme with `getTheme()` (the theme is registered when you pass it to the config): ```js import { mainTheme, getTheme } from 'handsontable/themes'; const hot = new Handsontable(container, { theme: mainTheme, }); getTheme('main') ?.setColorScheme('auto') ?.setDensityType('comfortable'); ``` **UMD build (script tags)** When using Handsontable via CDN or script tags, load the theme script after the main Handsontable script. The theme auto-registers itself, and you can retrieve it using `getTheme()`: ```html ``` ::: ::: only-for react ```jsx import { HotTable } from '@handsontable/react-wrapper'; import { mainTheme, registerTheme } from 'handsontable/themes'; const theme = registerTheme(mainTheme) .setColorScheme('auto') .setDensityType('comfortable'); function App() { return ( ); } ``` ::: ::: only-for angular ```ts import { mainTheme, registerTheme } from 'handsontable/themes'; const theme = registerTheme(mainTheme) .setColorScheme('auto') .setDensityType('comfortable'); // In your component @Component({ template: `` }) export class AppComponent { hotSettings = { theme: theme, // ... other options }; } ``` ::: ::: only-for vue ```ts import { markRaw, ref } from 'vue'; import { HotTable } from '@handsontable/vue3'; import { mainTheme, registerTheme } from 'handsontable/themes'; const theme = markRaw( registerTheme(mainTheme) .setColorScheme('auto') .setDensityType('comfortable') ); const hotSettings = ref({ theme: theme, // ... other options licenseKey: 'non-commercial-and-evaluation', }); ``` ```html ``` `markRaw()` is required here. Without it, Vue wraps the `ThemeBuilder` in a reactive proxy that can't reach the builder's private fields, and the grid throws `TypeError: attempted to get private field on non-instance` when it mounts. ::: ### Option 2: Using CSS files Alternatively, you can load theme CSS files and pass the theme name as a string to the `theme` option. #### Step 1. Load CSS files To ensure Handsontable renders correctly, it's required to load both the base and theme CSS files. The base file contains structural styles, while the theme file includes colors, sizes, and other variables needed for the grid. ```js // ESM (ECMAScript modules) import 'handsontable/styles/ht-theme-main.min.css'; // CommonJS require('handsontable/styles/ht-theme-main.min.css'); ``` ::: only-for angular The recommended approach for applying global styles is to include them in the `styles` array defined in the `angular.json` configuration file. ```json5 { // ... "styles": [ "src/styles.css", "node_modules/handsontable/styles/ht-theme-main.min.css" ], // ... } ``` ::: Alternatively, you can import the necessary files from the recommended CDN such as [JSDelivr](https://jsdelivr.com/package/npm/handsontable) or [cdnjs](https://cdnjs.com/libraries/handsontable). ```html ``` #### Step 2. Pass a theme name To use a theme, specify the theme name in the data grid's global settings object: ::: only-for javascript ```js const container = document.querySelector('#handsontable-example'); const hot = new Handsontable(container, { // theme name with obligatory `ht-theme-*` prefix theme: 'ht-theme-main', // other options }); ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```html ``` ::: ::: only-for vue ```ts const hotSettings = ref({ theme: 'ht-theme-main', // ... other options licenseKey: 'non-commercial-and-evaluation', }); ``` ```html ``` ::: ## Create your own theme Create your own theme when the built-in `main`, `classic`, and `horizon` themes do not match your design system. A custom theme starts from a built-in preset, so you override only the values that differ. If you used the `--ht-wrapper-border-width` and `--ht-wrapper-border-color` variables, note that Handsontable 18.0 removed them, together with the `wrapperBorderWidth` and `wrapperBorderColor` tokens. The grid no longer reads them, and a custom token with the same name has no effect. To keep a wrapper border, draw it in your own CSS. For details, see [Update theme wrapper border variables](@/guides/upgrade-and-migration/migrating-from-17.1-to-18.0/migrating-from-17.1-to-18.0.md#update-theme-wrapper-border-variables). ### Start from a preset Pass a built-in theme object to `registerTheme()` with a name for your theme. `registerTheme()` clones the preset and renames it, so the built-in theme stays unchanged: ```js import { mainTheme, registerTheme } from 'handsontable/themes'; const customTheme = registerTheme('custom-theme', mainTheme); ``` To start from another preset, import `classicTheme` or `horizonTheme` instead. Your theme keeps the density and icons of the preset: `main` uses the `'default'` density, `classic` uses `'compact'`, and `horizon` uses `'comfortable'`. ### Override values with `params()` Call `params()` to override only the colors and tokens you need. The values you pass are merged into your theme. `params()` returns the theme, so you can chain `setColorScheme()` and `setDensityType()` after it: ```js customTheme .params({ colors: { primary: { 500: '#9333ea', 600: '#7e22ce', }, }, tokens: { fontSize: '13px', borderRadius: 'sizing.size_2', }, }) .setColorScheme('light') .setDensityType('compact'); ``` ### Take full control with the static variables To mix parts of different presets, for example the `horizon` tokens with the `main` icons, import the `tokens`, `colors`, and `icons` files from `handsontable/themes/static/variables`. Then pass them to `registerTheme()`: ```js import tokens from 'handsontable/themes/static/variables/tokens/horizon'; import colors from 'handsontable/themes/static/variables/colors/horizon'; import icons from 'handsontable/themes/static/variables/icons/main'; import { registerTheme } from 'handsontable/themes'; const customTheme = registerTheme('custom-theme', { tokens, colors, icons, density: 'comfortable', colorScheme: 'auto', }); ``` These files hold only `tokens`, `colors`, and `icons`, so set `density` yourself. If you leave it out, your theme uses the `'default'` density, even when the files come from `classic` or `horizon`. The `classic` preset has `tokens` and `colors` files, and it uses the `main` icons. ### Config keys A theme configuration object uses these keys. The built-in theme objects already contain all the required ones: | Key | Required | Description | | ------------- | -------- | ------------------------------------------------------------------------------------------------------------ | | `tokens` | Yes | Design tokens for visual properties. | | `colors` | Yes | Color palette with nested color values. | | `icons` | Yes | Icon definitions. | | `sizing` | No | Size scale values (`size_0` through `size_10`). | | `density` | No | `'default'`, `'compact'`, `'comfortable'`, or a density configuration object. The default is `'default'`. | | `colorScheme` | No | `'light'`, `'dark'`, or `'auto'`. The default is `'auto'`. | The `name` is required too. When you call `registerTheme('custom-theme', config)`, the first argument sets it. You can set the name only at registration, not with `params()`. If you omit `tokens`, `colors`, or `icons`, `registerTheme()` throws a validation error. If a theme with the same name is already registered, `registerTheme()` logs a warning and returns the existing theme. For all keys, see [Theme API parameters](#theme-api-parameters). To reference other values in a token, see [Token value references](#token-value-references). ### Generate a starting point Instead of writing the configuration by hand, you can design a theme in the [Theme Builder](https://handsontable.com/theme-builder). Choose your values, then copy the code from **Generated code**, in the **JavaScript** tab, into your project. To learn more about registering and tuning a theme, see [Register a custom theme](@/guides/styling/theme-customization/theme-customization.md#register-a-custom-theme). ::: only-for javascript react angular For a full integration with a design system, see the [Handsontable with shadcn/ui](@/recipes/themes/custom-theme/custom-theme.md) recipe. ::: ### Use your registered theme Pass the theme that `registerTheme()` returns to the `theme` option. The registration step is the same in every framework. Only the way you pass the theme differs. For the full setup, see [Step 2. Configure the theme](#configure-the-theme). ::: only-for javascript ```js const hot = new Handsontable(container, { theme: customTheme, // ... other options }); ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts hotSettings = { theme: customTheme, // ... other options }; ``` ::: ::: only-for vue Wrap the theme in `markRaw()` before you pass it to the settings: ```ts import { markRaw, ref } from 'vue'; const hotSettings = ref({ theme: markRaw(customTheme), // ... other options licenseKey: 'non-commercial-and-evaluation', }); ``` ::: ## Switch between CSS-file themes at runtime Switch a grid to another CSS-file theme by giving it a different theme class name, either through [`useTheme()`](@/api/core.md#usetheme) or through the `theme` option. Both take a class name string such as `ht-theme-main-dark`, never a theme config object or a `ThemeBuilder` instance. In the framework wrappers, prefer changing the value your component binds over calling the instance directly. A wrapper re-asserts its bound settings as the component re-renders, which can undo an imperative call. ::: only-for javascript ```js import Handsontable from 'handsontable'; import 'handsontable/styles/ht-theme-main.min.css'; const hot = new Handsontable(container, { theme: 'ht-theme-main', // ... other options }); function toggleTheme(isDarkMode) { hot.useTheme(isDarkMode ? 'ht-theme-main-dark' : 'ht-theme-main'); } ``` ::: ::: only-for react ```jsx import { useState } from 'react'; import { HotTable } from '@handsontable/react-wrapper'; import 'handsontable/styles/ht-theme-main.min.css'; const App = () => { const [theme, setTheme] = useState('ht-theme-main'); const toggleTheme = (isDarkMode) => { setTheme(isDarkMode ? 'ht-theme-main-dark' : 'ht-theme-main'); }; return ( ); }; ``` Keep the theme in React state and pass it through the `theme` prop. The React wrapper re-sends the `theme` prop on every render, so a `useTheme()` call made through the instance ref is undone by the next render. ::: ::: only-for angular ```typescript import { Component } from '@angular/core'; import { GridSettings, HotTableModule } from '@handsontable/angular-wrapper'; import 'handsontable/styles/ht-theme-main.min.css'; @Component({ standalone: true, imports: [HotTableModule], template: ``, }) export class AppComponent { hotSettings: GridSettings = { theme: 'ht-theme-main' }; toggleTheme(isDarkMode: boolean) { this.hotSettings = { ...this.hotSettings, theme: isDarkMode ? 'ht-theme-main-dark' : 'ht-theme-main', }; } } ``` ::: ::: only-for vue ```ts import { ref } from 'vue'; import { HotTable } from '@handsontable/vue3'; import 'handsontable/styles/ht-theme-main.min.css'; const hotSettings = ref({ theme: 'ht-theme-main', }); function toggleTheme(isDarkMode) { hotSettings.value.theme = isDarkMode ? 'ht-theme-main-dark' : 'ht-theme-main'; } ``` ```html ``` ::: Load the matching static stylesheet for every class name you switch to. Light and dark modes of a theme live in the same file: `ht-theme-main.min.css` defines `ht-theme-main`, `ht-theme-main-dark`, and `ht-theme-main-dark-auto` (see [Step 1. Load CSS files](#step-1-load-css-files)). Without the file, the grid logs a warning that the theme's stylesheets are missing and loses its styling. `useTheme()` validates the format of the name, not whether a stylesheet for it exists. The two cases behave differently: | Value | Result | | --- | --- | | `useTheme('dark')` | Rejected. The grid keeps its current theme and logs `dark isn't a valid theme name. Please ensure it follows the format ht-theme-.` | | `useTheme('ht-theme-doesnotexist')` | Applied. The grid switches to a class that no stylesheet defines, loses its styling, and logs the missing-stylesheets warning. | ## Switch the color scheme at runtime with the Theme API When a grid uses the Theme API, call `setColorScheme()` directly on the `ThemeBuilder` instance returned by `registerTheme()`. The builder notifies the grid on its own -- no `updateSettings()` call is needed. ::: only-for javascript ```js import Handsontable from 'handsontable'; import { mainTheme, registerTheme } from 'handsontable/themes'; const theme = registerTheme(mainTheme); const hot = new Handsontable(container, { theme, // ... other options }); function toggleTheme(isDarkMode) { theme.setColorScheme(isDarkMode ? 'dark' : 'light'); } ``` ::: ::: only-for react ```jsx import { HotTable } from '@handsontable/react-wrapper'; import { mainTheme, registerTheme } from 'handsontable/themes'; const theme = registerTheme(mainTheme); const App = () => { const toggleTheme = (isDarkMode) => { theme.setColorScheme(isDarkMode ? 'dark' : 'light'); }; return ( ); }; ``` ::: ::: only-for angular ```typescript import { Component } from '@angular/core'; import { GridSettings, HotTableModule } from '@handsontable/angular-wrapper'; import { mainTheme, registerTheme } from 'handsontable/themes'; const mainThemeBuilder = registerTheme(mainTheme); @Component({ standalone: true, imports: [HotTableModule], template: ``, }) export class AppComponent { theme = mainThemeBuilder; hotSettings: GridSettings = { theme: this.theme }; toggleTheme(isDarkMode: boolean) { this.theme.setColorScheme(isDarkMode ? 'dark' : 'light'); } } ``` An Angular template resolves names against the component instance, so the builder has to reach it as a class field. A module-level `const` alone leaves the `theme` option `undefined`, and the grid silently falls back to the registered `main` theme. The settings object stays untouched when the color scheme changes: the builder notifies the grid by itself. ::: ::: only-for vue ```ts import { HotTable } from '@handsontable/vue3'; import { mainTheme, registerTheme } from 'handsontable/themes'; const theme = registerTheme(mainTheme); function toggleTheme(isDarkMode) { theme.setColorScheme(isDarkMode ? 'dark' : 'light'); } ``` ```html ``` ::: Don't mix this with [`useTheme()`](#switch-between-css-file-themes-at-runtime): the Theme API only ever generates styles under the plain `ht-theme-{name}` class, never a `-dark` variant of it, so switching to `ht-theme-{name}-dark` on a Theme API grid loses the theme instead of applying its dark mode. `setColorScheme()` changes the `ThemeBuilder` itself, so every grid that shares that builder switches with it. To change one grid only, leave the builder alone and set the per-instance [`colorScheme`](@/api/options.md#colorscheme) option, as described in [Switch the color scheme or density at runtime](#switch-the-color-scheme-or-density-at-runtime). ## Switch between two registered themes at runtime To swap the grid's theme for a genuinely different one -- not a light/dark variant of the same theme -- hand the target theme to the grid through the `theme` option: with [`updateSettings()`](@/api/core.md#updatesettings) on a plain instance, or by changing the value your component binds, so that the new theme survives the next render. ::: only-for javascript ```js import Handsontable from 'handsontable'; import { mainTheme, horizonTheme, registerTheme } from 'handsontable/themes'; const themeA = registerTheme(mainTheme); const themeB = registerTheme(horizonTheme); const hot = new Handsontable(container, { theme: themeA, // ... other options }); function switchTheme(useHorizon) { hot.updateSettings({ theme: useHorizon ? themeB : themeA }); } ``` ::: ::: only-for react ```jsx import { useState } from 'react'; import { HotTable } from '@handsontable/react-wrapper'; import { mainTheme, horizonTheme, registerTheme } from 'handsontable/themes'; const themeA = registerTheme(mainTheme); const themeB = registerTheme(horizonTheme); const App = () => { const [theme, setTheme] = useState(themeA); const switchTheme = (useHorizon) => { setTheme(useHorizon ? themeB : themeA); }; return ( ); }; ``` ::: ::: only-for angular ```typescript import { Component } from '@angular/core'; import { GridSettings, HotTableModule } from '@handsontable/angular-wrapper'; import { mainTheme, horizonTheme, registerTheme } from 'handsontable/themes'; const themeA = registerTheme(mainTheme); const themeB = registerTheme(horizonTheme); @Component({ standalone: true, imports: [HotTableModule], template: ``, }) export class AppComponent { hotSettings: GridSettings = { theme: themeA }; switchTheme(useHorizon: boolean) { this.hotSettings = { ...this.hotSettings, theme: useHorizon ? themeB : themeA }; } } ``` Bind a settings field and replace it, rather than calling `updateSettings()` on the instance. An object literal written straight into `[settings]="{ ... }"` is restated whenever anything else inside it changes, which undoes the imperative call. ::: ::: only-for vue ```ts import { ref } from 'vue'; import { HotTable } from '@handsontable/vue3'; import { mainTheme, horizonTheme } from 'handsontable/themes'; const hotSettings = ref({ theme: mainTheme, }); function switchTheme(useHorizon) { hotSettings.value.theme = useHorizon ? horizonTheme : mainTheme; } ``` ```html ``` Pass the plain theme config objects here, not the `ThemeBuilder` instances that `registerTheme()` returns, and never put a `ThemeBuilder` inside `ref()` or `reactive()`. Vue wraps such an object in a reactive proxy, and the builder keeps its state in private class fields that a proxy can't reach, so the grid throws `TypeError: attempted to get private field on non-instance` when it mounts. Wrapping the builder in `markRaw()` stops the error but doesn't make the swap work: the Vue wrapper compares settings with `JSON.stringify()`, and because a builder exposes no public fields, every builder serializes to `{}` and the change looks like no change at all. The grid registers the config you pass, so re-applying a config whose theme is already registered logs `Theme "main" is already registered. Registration skipped.` and reuses the theme that is already there. ::: Passing a theme object works only when no `ht-theme-*` class is set on the grid's container element or on any of its ancestors. If one is, the grid stays on that CSS-file theme, and the passed theme object is ignored without a warning. Remove the class from your own markup before switching themes this way. ## Set the color scheme or density without a theme If the only thing you want to change is the color scheme or the amount of white space, you don't have to import, register, and configure a theme. Set the [`colorScheme`](@/api/options.md#colorscheme) or [`density`](@/api/options.md#density) option directly, and the grid applies it on top of the theme it already uses. | Option | Allowed values | Default | | ------------- | ----------------------------------------- | ------------------------------ | | `colorScheme` | `'light'`, `'dark'`, `'auto'` | the color scheme of your theme | | `density` | `'default'`, `'compact'`, `'comfortable'` | the density of your theme | Both options are per-instance overrides. The theme itself stays unchanged, so other grids that use the same theme keep their own color scheme and density. ::: only-for javascript ```js const container = document.querySelector('#handsontable-example'); const hot = new Handsontable(container, { colorScheme: 'dark', density: 'compact', // other options }); ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```html ``` ::: ::: only-for vue ```ts const hotSettings = ref({ colorScheme: 'dark', density: 'compact', // ... other options licenseKey: 'non-commercial-and-evaluation', }); ``` ```html ``` ::: ### Switch the color scheme or density at runtime Pass either option to [`updateSettings()`](@/api/core.md#updatesettings). This is all you need for a dark mode toggle: ```js hot.updateSettings({ colorScheme: 'dark', }); ``` An unsupported value is ignored, and the grid logs a warning naming the option. It does not throw, so one bad value does not break the rest of the update. To drop an override and go back to the value your theme defines, set the option to `undefined`: ```js hot.updateSettings({ colorScheme: undefined, density: undefined, }); ``` ### When these options apply Both options are features of the Theme API, so they need the theme engine to be active. The engine is active when you leave the [`theme`](@/api/options.md#theme) option out, or when you pass a theme config object or a `ThemeBuilder` instance to it. It is not active when the theme comes from a CSS class name — either the `theme` option set to a string such as `'ht-theme-main'`, or an `ht-theme-*` class on the container element. In that case the grid logs a warning and the options have no effect. Apart from that, it doesn't matter which stylesheets you load. Both options work with the base stylesheet alone and with a theme stylesheet on top of it, minified or not. ::: only-for angular ## Global theme management In addition to passing a theme via the settings object for individual Handsontable instances, you can set a global default theme that applies to all instances. This can be accomplished in two ways: ### Using ApplicationConfig You can use `ApplicationConfig` to provide a global configuration via the `HOT_GLOBAL_CONFIG` injection token. ```ts import { bootstrapApplication } from '@angular/platform-browser'; import { ApplicationConfig } from '@angular/core'; import { HOT_GLOBAL_CONFIG, HotGlobalConfig } from '@handsontable/angular-wrapper'; import { mainTheme, registerTheme } from 'handsontable/themes'; import { AppComponent } from './app/app.component'; const theme = registerTheme(mainTheme).setColorScheme('auto'); export const appConfig: ApplicationConfig = { providers: [{ provide: HOT_GLOBAL_CONFIG, useValue: { theme: theme } as HotGlobalConfig }], }; bootstrapApplication(AppComponent, appConfig); ``` This global configuration is then merged with local settings when initializing each Handsontable instance. ### Using HotGlobalConfigService You can manage the global theme at runtime using the `HotGlobalConfigService`. ```ts import { horizonTheme, registerTheme } from 'handsontable/themes'; const theme = registerTheme(horizonTheme).setColorScheme('dark'); hotConfigService.setConfig({ theme: theme }); ``` When the configuration changes, all Handsontable instances will automatically update their settings. ### Theme settings hierarchy When both a global theme and a local theme are defined, the local setting takes precedence. This means: - Local Setting: If a `` component is provided with a `theme` via its `settings` input, that value overrides the global default. - Global Setting: If no local theme is specified, the component falls back to the global configuration provided via the injection token or the configuration service. This hierarchy ensures that you can define a consistent default theme for your entire application while still allowing individual components to customize their appearance when needed. ::: ## Theme reference ### Theme API parameters When registering a theme with `registerTheme()` or updating it using the `params()` method, you can configure the following keys: | Key | Description | | ------------- | ---------------------------------------------------------------------------------------------- | | `name` | Theme name string (can only be set during `registerTheme()`, cannot be updated via `params()`) | | `sizing` | Size scale values (`size_0` through `size_10`) | | `density` | Density type (`'default'`, `'compact'`, `'comfortable'`) or density configuration object | | `icons` | SVG icon definitions | | `colors` | Color palette with nested color values | | `tokens` | Design tokens for visual properties | | `colorScheme` | Color scheme (`'light'`, `'dark'`, or `'auto'`) | #### Token value references Token values support a powerful reference system using dot notation. Instead of hardcoding values, you can reference values from other configuration namespaces: | Reference pattern | Example | Description | | ----------------- | -------------------------- | ---------------------------------- | | Direct value | `'14px'` | Use a literal CSS value | | `tokens.*` | `'tokens.foregroundColor'` | Reference another token value | | `sizing.*` | `'sizing.size_4'` | Reference a sizing scale value | | `colors.*` | `'colors.primary.500'` | Reference a color from the palette | | `density.*` | `'density.gap'` | Reference a density-specific value | ##### Sizing references The sizing scale provides consistent spacing values: ```js myTheme.params({ tokens: { iconSize: 'sizing.size_5', // References e.g. 20px borderRadius: 'sizing.size_2', // References e.g. 8px }, }); ``` ##### Color references Colors use a hierarchical structure with dot notation for nested values: ```js myTheme.params({ tokens: { accentColor: 'colors.primary.500', // References primary color borderColor: 'colors.palette.200', // References palette color backgroundColor: 'colors.white', // References base white color }, }); ``` ##### Density references Density values adjust spacing based on the selected density type: ```js myTheme.params({ tokens: { cellHorizontalPadding: 'density.cellHorizontal', buttonVerticalPadding: 'density.buttonVertical', gapSize: 'density.gap', }, }); ``` ##### Token cross-references Tokens can reference other tokens for consistent styling: ```js myTheme.params({ tokens: { barForegroundColor: 'tokens.foregroundColor', cellEditorBackgroundColor: 'tokens.backgroundColor', }, }); ``` #### Light and dark mode values For tokens that should have different values in light and dark modes, use an array with two values where the first value is for light mode and the second is for dark mode: ```js myTheme.params({ tokens: { // [lightModeValue, darkModeValue] borderColor: ['colors.palette.100', 'colors.palette.700'], foregroundColor: ['colors.palette.800', 'colors.palette.200'], backgroundColor: ['colors.white', 'colors.palette.950'], }, }); ``` ### CSS files Handsontable provides CSS files needed to style your data grid. Here's an overview of what's available: #### Base CSS file - **`handsontable.css`** / **`handsontable.min.css`** - The base stylesheet containing all structural styles, layout rules, and core functionality. This file is auto-injected by default. You can inject it manually instead, but you must set [`injectCoreCss`](@/api/options.md#injectcorecss) to `false` first. It includes border styles, cell rendering rules, and other fundamental grid components. #### Theme files All themes are available in two variants: - **`ht-theme-{name}.css`** / **`ht-theme-{name}.min.css`** - Complete theme with icons included (where `{name}` is `main`, `horizon`, or `classic`). - **`ht-theme-{name}-no-icons.css`** / **`ht-theme-{name}-no-icons.min.css`** - Theme without icon styles. #### Icon files If you're using a theme without icons (`*-no-icons.css`), you can optionally load separate icon files: - **`ht-icons-{name}.css`** / **`ht-icons-{name}.min.css`** - Icon styles for the theme (where `{name}` is `main` or `horizon`). #### Recommended usage For production, use the minified versions (`.min.css`) to reduce file size and improve load times. For development, you may prefer the unminified versions (`.css`) for easier debugging. ## The legacy theme The legacy CSS file ([`handsontable.full.min.css`](https://github.com/handsontable/handsontable/blob/master/handsontable/dist/handsontable.full.min.css)) was the default styles up until `version 15` (released in December 2024). These styles are legacy and are removed in version 17.0.0. ## Known limitations In some cases, global styles enforced by the browser or operating system can impact the appearance of the data grid. This is a common challenge faced by all websites, not just Handsontable. Here are two specific scenarios and how to handle them: - **High contrast mode in Windows**: To style the component when Windows' high contrast mode is active, use the `forced-colors` media query. This allows you to detect and adapt to forced color settings. [Read more](https://blogs.windows.com/msedgedev/2020/09/17/styling-for-windows-high-contrast-with-new-standards-for-forced-colors/) - **Auto dark theme in Google Chrome**: Chrome automatically applies a dark theme in some scenarios. To detect and manage this behavior, refer to the official [Chrome guide](https://developer.chrome.com/blog/auto-dark-theme) ## Result Your grid now renders with the theme you configured. You can switch color schemes and density modes at runtime using the Theme API. ## Related blog articles
- [From components to tables: Designing a data table component in your design system](https://handsontable.com/blog/from-components-to-tables-designing-a-data-table-component-in-your-design-system) - [Handsontable 15.0.0: Introducing Themes and Functional React Wrapper](https://handsontable.com/blog/handsontable-15.0.0-introducing-themes-and-functional-react-wrapper) - [Handsontable 16.2.0: Simplified Theming and Advanced User Notifications](https://handsontable.com/blog/handsontable-16.2.0-simplified-theming-and-advanced-user-notifications)
## Troubleshooting Didn't find what you need? Try this:
- [View related topics](https://github.com/handsontable/handsontable/issues/) on GitHub - [Report an issue](https://github.com/handsontable/handsontable/issues/new/choose) on GitHub - [Start a discussion](https://forum.handsontable.com/c/getting-help/questions) on Handsontable's forum - [Contact our technical support](https://handsontable.com/contact?category=technical_support) to get help
--- # Design system URL: https://handsontable.com/docs/javascript-data-grid/handsontable-design-system/ The Handsontable design system defines the tokens, themes, and components that control the grid visual appearance. Read this to understand how themes and CSS variables relate. [[toc]] ## Overview The Handsontable Design System is a complete toolkit for building, prototyping, and customizing data grids with Handsontable. It includes grid components and design tokens, making it easy to fit Handsontable into your app’s layout or customize it in ways that wouldn’t be possible without built-in features like auto layout, variables, and more. ## Live preview Open the Design System in Figma ## Use tokens to generate a theme Inside the Figma file, you’ll find local variables that define all parts of the data grid — like colors, spacing, font styles, icon sizes, and more. These variables are organized into three themes: Main, Horizon and Classic each available in both light and dark mode. You can tweak the variables however you like to match your brand or product style. Once you’re happy with the changes, export them as JSON tokens. We recommend using the [Design Tokens](https://www.figma.com/community/plugin/888356646278934516/design-tokens) plugin from the Figma Community — it’s straightforward and does the job well. After exporting, use our [Theme Generator](https://github.com/handsontable/handsontable/tree/develop/handsontable/scripts/themes/figma), located in the Handsontable monorepo at `handsontable/scripts/themes/figma`, to convert your tokens into a CSS theme and JavaScript variables object that works with Handsontable. Run it with `npm run generate:themes` from the `handsontable/` package root. ![design_system_light](/img/design_system_light.png) ![design_system_dark](/img/design_system_dark.png) ## Updates frequency The design system is our primary reference when planning new features or redesigns, and it’s always kept up to date. In some cases, we update the design system independently of product releases to enhance consistency and streamline the design workflow. ## Known limitations - The legacy style is not part of the design system. Starting from version 17.0.0, legacy styles are no longer supported and you need to migrate to the Classic theme. ## Related blog articles
- [From components to tables: Designing a data table component in your design system](https://handsontable.com/blog/from-components-to-tables-designing-a-data-table-component-in-your-design-system)
## Troubleshooting Didn't find what you need? Try this:
- [View related topics](https://github.com/handsontable/handsontable/issues/) on GitHub - [Report an issue](https://github.com/handsontable/handsontable/issues/new/choose) on GitHub - [Start a discussion](https://forum.handsontable.com/c/getting-help/questions) on Handsontable's forum - [Contact our technical support](https://handsontable.com/contact?category=technical_support) to get help
## Related
- [Themes](@/guides/styling/themes/themes.md) - [Legacy Style](@/guides/styling/legacy-style/legacy-style.md)
--- # Theme customization URL: https://handsontable.com/docs/javascript-data-grid/theme-customization/ Customize Handsontable's appearance using the Theme API, Figma Theme Generator, CSS variables, or the visual Theme Builder. [[toc]] ## Overview CSS variables provide a powerful and flexible way to customize Handsontable's appearance by adjusting design elements such as colors, spacing, borders, and typography to match your application's design system. These variables give you granular control over every visual aspect of the data grid, from basic styling to advanced component customization. We provide multiple approaches for leveraging CSS variables to create any look that your designer can imagine. From quick theme modifications to completely custom designs, your options include: 1. **Theme API** – Modify themes via configuration parameters or register your own custom theme programmatically. 2. **Figma Theme Generator** – Modify design variables in Figma and export them using the Figma Theme Generator tool. 3. **Override CSS variables** – Directly override CSS variables or edit CSS files from the `/styles` directory for full control. 4. **Theme Builder UI** – Use the online [Theme Builder](https://handsontable.com/theme-builder) to visually customize and export your theme without writing code. The data grid's styling system is built entirely on CSS variables, with over 200 variables organized into logical categories covering typography, colors, spacing, borders, and component-specific styling listed below. ## Option 1: Theme API The Theme API allows you to customize themes programmatically by registering custom themes and configuring them at runtime. You can use the `theme` option with a `ThemeBuilder` object for dynamic configuration. ### Register a custom theme Use `registerTheme` to create a custom theme with your own configuration: ::: only-for javascript ```js import Handsontable from 'handsontable'; import { mainTheme, registerTheme } from 'handsontable/themes'; // Register main theme const myTheme = registerTheme(mainTheme); // Configure the theme at runtime myTheme.setColorScheme('light'); // 'light', 'dark', or 'auto' myTheme.setDensityType('default'); // 'compact', 'default', or 'comfortable' const hot = new Handsontable(container, { theme: myTheme, // other options }); ``` ::: ::: only-for react ```jsx import Handsontable from 'handsontable'; import { HotTable } from '@handsontable/react-wrapper'; import { mainTheme, registerTheme } from 'handsontable/themes'; // Register main theme const myTheme = registerTheme(mainTheme); // Configure the theme at runtime myTheme.setColorScheme('light'); myTheme.setDensityType('default'); function App() { return ( ); } ``` ::: ::: only-for angular ```ts import Handsontable from 'handsontable'; import { mainTheme, registerTheme } from 'handsontable/themes'; // Register main theme const myTheme = registerTheme(mainTheme); // Configure the theme at runtime myTheme.setColorScheme('light'); myTheme.setDensityType('default'); ``` ```html ``` ::: ::: only-for vue ```ts import { ref } from 'vue'; import { HotTable } from '@handsontable/vue3'; import { registerAllModules } from 'handsontable/registry'; import { mainTheme, registerTheme } from 'handsontable/themes'; registerAllModules(); const myTheme = registerTheme(mainTheme); myTheme.setColorScheme('light'); myTheme.setDensityType('default'); const hotSettings = ref({ theme: myTheme, // other options licenseKey: 'non-commercial-and-evaluation', }); ``` ```html ``` ::: ### Configure theme parameters Use the `params()` method to update theme parameters dynamically: ```js myTheme.params({ colors: { primary: { 500: '#9333ea', // Change primary color }, }, tokens: { fontSize: '16px', iconSize: 'sizing.size_5', borderColor: ['colors.primary.500', 'colors.primary.600'], }, }); ``` ### Theme API example The following example demonstrates using the Theme API to register a theme with a custom purple accent color: ::: only-for javascript ::: example #example2 --html 1 --js 2 --ts 3 @[code](@/content/guides/styling/theme-customization/javascript/example2.html) @[code](@/content/guides/styling/theme-customization/javascript/example2.js) @[code](@/content/guides/styling/theme-customization/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 .disable-auto-theme :react --js 1 --ts 2 @[code](@/content/guides/styling/theme-customization/react/example2.jsx) @[code](@/content/guides/styling/theme-customization/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/styling/theme-customization/angular/example2.ts) @[code](@/content/guides/styling/theme-customization/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 .disable-auto-theme :vue3 @[code](@/content/guides/styling/theme-customization/vue/example2.vue) ::: ::: ## Option 2: Figma Theme Generator The Figma Theme Generator allows designers and developers to work together seamlessly by exporting design tokens directly from Figma into a CSS theme file. ### Export themes from Figma To create a new theme or modify an existing one in Figma: 1. Open your Figma design file and modify the design variables (colors, spacing, typography, etc.) to match your requirements. 2. Export the design tokens as JSON using Figma's built-in variables export or a plugin. 3. Use the [Handsontable Theme Generator](https://github.com/handsontable/handsontable/tree/develop/handsontable/scripts/themes/figma), located in the Handsontable monorepo at `handsontable/scripts/themes/figma`, to convert your Figma tokens into CSS theme files or JS variable objects. Run it with `npm run generate:themes` from the `handsontable/` package root. The Theme Generator transforms JSON tokens exported from Figma into properly formatted theme files that work with Handsontable. It outputs CSS files (with or without icons) and JS variable files for colors, tokens, and icons. This approach is ideal for teams where designers define the visual language in Figma and developers implement it in code. ## Option 3: Override CSS variables For full control over your theme, you can override CSS variables directly. Follow these [steps](@/guides/styling/themes/themes.md#use-a-theme) to apply a theme, then override the variables for your chosen theme. You can also open the CSS theme files in `handsontable/styles/` to see every CSS variable a theme defines. Copy the variables you need into your own stylesheet - edits made inside `node_modules` are lost on the next install. Here's an example for `.ht-theme-main`: ::: only-for javascript ::: example #example1 --html 1 --js 2 --ts 3 @[code](@/content/guides/styling/theme-customization/javascript/example1.html) @[code](@/content/guides/styling/theme-customization/javascript/example1.js) @[code](@/content/guides/styling/theme-customization/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 .disable-auto-theme :react --js 1 --ts 2 @[code](@/content/guides/styling/theme-customization/react/example1.jsx) @[code](@/content/guides/styling/theme-customization/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/styling/theme-customization/angular/example1.ts) @[code](@/content/guides/styling/theme-customization/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 .disable-auto-theme :vue3 @[code](@/content/guides/styling/theme-customization/vue/example1.vue) ::: ::: ### Target the theme class, not the container Set your overrides on the theme class. Handsontable renders an inner wrapper that also carries the theme class, and that wrapper redefines every variable the theme owns. A value you set on the container element is inherited by the wrapper and then overwritten there, so it never reaches the cells: ```css /* No effect - the inner wrapper redefines these variables. */ #my-grid { --ht-font-size: 24px; --ht-line-height: 32px; } ``` Raising the specificity of that selector does not help, because the wrapper's own declaration is closer to the cells rather than weaker. Scope your rule to the theme class instead. Writing it as a descendant of your container keeps the override local to one grid and independent of stylesheet order: ```css #my-grid .ht-theme-main { --ht-font-size: 24px; --ht-line-height: 32px; } ``` A bare `.ht-theme-main { ... }` rule works as well, but it matches the wrapper with the same specificity as the theme's own rule. It wins only when your stylesheet comes after the theme stylesheet, which depends on how your bundler orders CSS. ## Option 4: Use the Theme Builder UI If you prefer a visual approach to creating themes, use the [Handsontable Theme Builder](https://handsontable.com/theme-builder). This online tool provides an intuitive interface for customizing colors, spacing, and other theme properties without writing code. Once you're satisfied with your design, you can export the generated your theme and integrate it into your project. ## Variables reference Handsontable provides a comprehensive set of JS and CSS variables that let you customize the appearance of every component. **CSS Variable** — The names of the CSS custom properties (e.g. `--ht-sizing-size-1`) you can override in your stylesheet. **JS Option** — The values in this column are the keys you use when customizing a theme via the Theme API. Call `theme.params()` on your registered theme and pass an object where each key is nested under one of: - **`sizing`** — spacing and size scale (e.g. `sizing.size_1`) - **`density.sizes.default`**, **`density.sizes.compact`**, or **`density.sizes.comfortable`** — density-specific spacing (e.g. `cellVertical`, `gap`) - **`colors`** — color palette (e.g. `colors.primary.500`) - **`tokens`** — design tokens used by components (e.g. `tokens.fontSize`) Example: to override the `tokens.gapSize`, use the JS Option like this: ```js myTheme.params({ tokens: { gapSize: 'sizing.size_1' } }) ```
### Sizing variables | Variable | Description | | -------- | ----------------------- | |
CSS: `--ht-sizing-size-0`
JS: `size_0`
| Zero size (0px) | |
CSS: `--ht-sizing-size-0-25`
JS: `size_0_25`
| Quarter unit size (1px) | |
CSS: `--ht-sizing-size-0-5`
JS: `size_0_5`
| Half unit size (2px) | |
CSS: `--ht-sizing-size-1`
JS: `size_1`
| Base unit size (4px) | |
CSS: `--ht-sizing-size-1-5`
JS: `size_1_5`
| 1.5x unit size (6px) | |
CSS: `--ht-sizing-size-2`
JS: `size_2`
| 2x unit size (8px) | |
CSS: `--ht-sizing-size-3`
JS: `size_3`
| 3x unit size (12px) | |
CSS: `--ht-sizing-size-4`
JS: `size_4`
| 4x unit size (16px) | |
CSS: `--ht-sizing-size-5`
JS: `size_5`
| 5x unit size (20px) | |
CSS: `--ht-sizing-size-6`
JS: `size_6`
| 6x unit size (24px) | |
CSS: `--ht-sizing-size-7`
JS: `size_7`
| 7x unit size (28px) | |
CSS: `--ht-sizing-size-8`
JS: `size_8`
| 8x unit size (32px) | |
CSS: `--ht-sizing-size-9`
JS: `size_9`
| 9x unit size (36px) | |
CSS: `--ht-sizing-size-10`
JS: `size_10`
| 10x unit size (40px) | ### Density variables | Variable | Description | | -------- | --------------------------------- | |
CSS: `--ht-density-cell-vertical`
JS: `cellVertical`
| Vertical padding for cells | |
CSS: `--ht-density-cell-horizontal`
JS: `cellHorizontal`
| Horizontal padding for cells | |
CSS: `--ht-density-bars-horizontal`
JS: `barsHorizontal`
| Horizontal padding for bars | |
CSS: `--ht-density-bars-vertical`
JS: `barsVertical`
| Vertical padding for bars | |
CSS: `--ht-density-gap`
JS: `gap`
| Standard gap size | |
CSS: `--ht-density-button-horizontal`
JS: `buttonHorizontal`
| Horizontal padding for buttons | |
CSS: `--ht-density-button-vertical`
JS: `buttonVertical`
| Vertical padding for buttons | |
CSS: `--ht-density-dialog-horizontal`
JS: `dialogHorizontal`
| Horizontal padding for dialogs | |
CSS: `--ht-density-dialog-vertical`
JS: `dialogVertical`
| Vertical padding for dialogs | |
CSS: `--ht-density-input-horizontal`
JS: `inputHorizontal`
| Horizontal padding for inputs | |
CSS: `--ht-density-input-vertical`
JS: `inputVertical`
| Vertical padding for inputs | |
CSS: `--ht-density-menu-vertical`
JS: `menuVertical`
| Vertical padding for menus | |
CSS: `--ht-density-menu-horizontal`
JS: `menuHorizontal`
| Horizontal padding for menus | |
CSS: `--ht-density-menu-item-vertical`
JS: `menuItemVertical`
| Vertical padding for menu items | |
CSS: `--ht-density-menu-item-horizontal`
JS: `menuItemHorizontal`
| Horizontal padding for menu items | ### Color palette variables | Variable | Description | | -------- | --------------------------- | |
CSS: `--ht-colors-white`
JS: `white`
| Pure white color | |
CSS: `--ht-colors-black`
JS: `black`
| Pure black color | |
CSS: `--ht-colors-transparent`
JS: `transparent`
| Transparent color | |
CSS: `--ht-colors-primary-100`
JS: `primary.100`
| Lightest primary accent | |
CSS: `--ht-colors-primary-200`
JS: `primary.200`
| Light primary accent | |
CSS: `--ht-colors-primary-300`
JS: `primary.300`
| Medium-light primary accent | |
CSS: `--ht-colors-primary-400`
JS: `primary.400`
| Medium primary accent | |
CSS: `--ht-colors-primary-500`
JS: `primary.500`
| Main primary accent | |
CSS: `--ht-colors-primary-600`
JS: `primary.600`
| Dark primary accent | |
CSS: `--ht-colors-palette-50`
JS: `palette.50`
| Lightest gray | |
CSS: `--ht-colors-palette-100`
JS: `palette.100`
| Very light gray | |
CSS: `--ht-colors-palette-200`
JS: `palette.200`
| Light gray | |
CSS: `--ht-colors-palette-300`
JS: `palette.300`
| Medium-light gray | |
CSS: `--ht-colors-palette-400`
JS: `palette.400`
| Medium gray | |
CSS: `--ht-colors-palette-500`
JS: `palette.500`
| Medium-dark gray | |
CSS: `--ht-colors-palette-600`
JS: `palette.600`
| Dark gray | |
CSS: `--ht-colors-palette-700`
JS: `palette.700`
| Darker gray | |
CSS: `--ht-colors-palette-800`
JS: `palette.800`
| Very dark gray | |
CSS: `--ht-colors-palette-900`
JS: `palette.900`
| Near black | |
CSS: `--ht-colors-palette-950`
JS: `palette.950`
| Darkest gray | ### Tokens variables #### Typography variables | Variable | Description | | -------- | ------------------------------------ | |
CSS: `--ht-font-family`
JS: `fontFamily`
| Font family for all text elements | |
CSS: `--ht-font-size`
JS: `fontSize`
| Base font size for all text elements | |
CSS: `--ht-font-size-small`
JS: `fontSizeSmall`
| Font size for smaller text | |
CSS: `--ht-line-height`
JS: `lineHeight`
| Line height for text elements | |
CSS: `--ht-line-height-small`
JS: `lineHeightSmall`
| Line height for smaller text | |
CSS: `--ht-font-weight`
JS: `fontWeight`
| Font weight for text elements | |
CSS: `--ht-letter-spacing`
JS: `letterSpacing`
| Letter spacing for text elements | #### Layout & spacing variables | Variable | Description | | -------- | ----------------------------------------------- | |
CSS: `--ht-gap-size`
JS: `gapSize`
| Standard gap size used throughout the component | |
CSS: `--ht-icon-size`
JS: `iconSize`
| Size of icons throughout the interface | |
CSS: `--ht-table-transition`
JS: `tableTransition`
| Transition duration for table animations | |
CSS: `--ht-border-radius`
JS: `borderRadius`
| Border radius of the table | #### Color system variables | Variable | Description | | -------- | ---------------------------------------------------------- | |
CSS: `--ht-border-color`
JS: `borderColor`
| Default border color for all elements | |
CSS: `--ht-accent-color`
JS: `accentColor`
| Primary accent color used for highlights and active states | |
CSS: `--ht-foreground-color`
JS: `foregroundColor`
| Primary text color | |
CSS: `--ht-foreground-secondary-color`
JS: `foregroundSecondaryColor`
| Secondary text color | |
CSS: `--ht-background-color`
JS: `backgroundColor`
| Primary background color | |
CSS: `--ht-background-secondary-color`
JS: `backgroundSecondaryColor`
| Secondary background color | |
CSS: `--ht-placeholder-color`
JS: `placeholderColor`
| Color for placeholder text | |
CSS: `--ht-read-only-color`
JS: `readOnlyColor`
| Color for read-only text | |
CSS: `--ht-disabled-color`
JS: `disabledColor`
| Color for disabled elements | #### Shadow variables | Variable | Description | | -------- | ---------------------------- | |
CSS: `--ht-shadow-color`
JS: `shadowColor`
| Base color used for shadows | |
CSS: `--ht-shadow-x`
JS: `shadowX`
| Horizontal offset of shadows | |
CSS: `--ht-shadow-y`
JS: `shadowY`
| Vertical offset of shadows | |
CSS: `--ht-shadow-blur`
JS: `shadowBlur`
| Blur radius of shadows | |
CSS: `--ht-shadow-opacity`
JS: `shadowOpacity`
| Opacity of shadows | #### Bar variables | Variable | Description | | -------- | -------------------------------------- | |
CSS: `--ht-bar-foreground-color`
JS: `barForegroundColor`
| Foreground color of bar elements | |
CSS: `--ht-bar-background-color`
JS: `barBackgroundColor`
| Background color of bar elements | |
CSS: `--ht-bar-horizontal-padding`
JS: `barHorizontalPadding`
| Horizontal padding inside bar elements | |
CSS: `--ht-bar-vertical-padding`
JS: `barVerticalPadding`
| Vertical padding inside bar elements | #### Cell border variables | Variable | Description | | -------- | -------------------------------- | |
CSS: `--ht-cell-horizontal-border-color`
JS: `cellHorizontalBorderColor`
| Color of horizontal cell borders | |
CSS: `--ht-cell-vertical-border-color`
JS: `cellVerticalBorderColor`
| Color of vertical cell borders | #### Row styling variables | Variable | Description | | -------- | ------------------------------------- | |
CSS: `--ht-row-header-odd-background-color`
JS: `rowHeaderOddBackgroundColor`
| Background color for odd row headers | |
CSS: `--ht-row-header-even-background-color`
JS: `rowHeaderEvenBackgroundColor`
| Background color for even row headers | |
CSS: `--ht-row-cell-odd-background-color`
JS: `rowCellOddBackgroundColor`
| Background color for odd row cells | |
CSS: `--ht-row-cell-even-background-color`
JS: `rowCellEvenBackgroundColor`
| Background color for even row cells | #### Cell padding variables | Variable | Description | | -------- | ------------------------------- | |
CSS: `--ht-cell-horizontal-padding`
JS: `cellHorizontalPadding`
| Horizontal padding inside cells | |
CSS: `--ht-cell-vertical-padding`
JS: `cellVerticalPadding`
| Vertical padding inside cells | #### Cell editor variables | Variable | Description | | -------- | ----------------------------------- | |
CSS: `--ht-cell-editor-border-width`
JS: `cellEditorBorderWidth`
| Border width of cell editors | |
CSS: `--ht-cell-editor-border-color`
JS: `cellEditorBorderColor`
| Border color of cell editors | |
CSS: `--ht-cell-editor-foreground-color`
JS: `cellEditorForegroundColor`
| Text color in cell editors | |
CSS: `--ht-cell-editor-background-color`
JS: `cellEditorBackgroundColor`
| Background color of cell editors | |
CSS: `--ht-cell-editor-shadow-blur-radius`
JS: `cellEditorShadowBlurRadius`
| Shadow blur radius for cell editors | |
CSS: `--ht-cell-editor-shadow-color`
JS: `cellEditorShadowColor`
| Shadow color for cell editors | #### Cell state variables | Variable | Description | | -------- | ----------------------------------------------- | |
CSS: `--ht-cell-success-background-color`
JS: `cellSuccessBackgroundColor`
| Background color for successful cell operations | |
CSS: `--ht-cell-error-background-color`
JS: `cellErrorBackgroundColor`
| Background color for error states in cells | |
CSS: `--ht-cell-read-only-background-color`
JS: `cellReadOnlyBackgroundColor`
| Background color for read-only cells | #### Cell selection variables | Variable | Description | | -------- | ----------------------------------- | |
CSS: `--ht-cell-selection-border-color`
JS: `cellSelectionBorderColor`
| Border color for selected cells | |
CSS: `--ht-cell-selection-background-color`
JS: `cellSelectionBackgroundColor`
| Background color for selected cells | #### Cell autofill variables | Variable | Description | | -------- | --------------------------------------- | |
CSS: `--ht-cell-autofill-size`
JS: `cellAutofillSize`
| Size of the autofill handle | |
CSS: `--ht-cell-autofill-hit-area-size`
JS: `cellAutofillHitAreaSize`
| Size of the autofill hit area | |
CSS: `--ht-cell-autofill-border-width`
JS: `cellAutofillBorderWidth`
| Border width of autofill elements | |
CSS: `--ht-cell-autofill-border-radius`
JS: `cellAutofillBorderRadius`
| Border radius of autofill elements | |
CSS: `--ht-cell-autofill-border-color`
JS: `cellAutofillBorderColor`
| Border color of autofill elements | |
CSS: `--ht-cell-autofill-background-color`
JS: `cellAutofillBackgroundColor`
| Background color of autofill elements | |
CSS: `--ht-cell-autofill-fill-border-color`
JS: `cellAutofillFillBorderColor`
| Border color of autofill fill indicator | #### Cell mobile handle variables | Variable | Description | | -------- | ------------------------------------ | |
CSS: `--ht-cell-mobile-handle-size`
JS: `cellMobileHandleSize`
| Size of mobile touch handles | |
CSS: `--ht-cell-mobile-handle-border-width`
JS: `cellMobileHandleBorderWidth`
| Border width of mobile handles | |
CSS: `--ht-cell-mobile-handle-border-radius`
JS: `cellMobileHandleBorderRadius`
| Border radius of mobile handles | |
CSS: `--ht-cell-mobile-handle-border-color`
JS: `cellMobileHandleBorderColor`
| Border color of mobile handles | |
CSS: `--ht-cell-mobile-handle-background-color`
JS: `cellMobileHandleBackgroundColor`
| Background color of mobile handles | |
CSS: `--ht-cell-mobile-handle-background-opacity`
JS: `cellMobileHandleBackgroundOpacity`
| Background opacity of mobile handles | #### Cell selection handle variables These variables style the handles shown at each edge midpoint of a selected range when the [`selectionHandles`](@/api/options.md#selectionhandles) option is on. They are separate from the mobile touch handles above. | Variable | Description | | -------- | ------------------------------------ | |
CSS: `--ht-cell-selection-handle-size`
JS: `cellSelectionHandleSize`
| Thickness of selection handles | |
CSS: `--ht-cell-selection-handle-length`
JS: `cellSelectionHandleLength`
| Length of selection handles | |
CSS: `--ht-cell-selection-handle-border-width`
JS: `cellSelectionHandleBorderWidth`
| Border width of selection handles | |
CSS: `--ht-cell-selection-handle-border-radius`
JS: `cellSelectionHandleBorderRadius`
| Border radius of selection handles | |
CSS: `--ht-cell-selection-handle-border-color`
JS: `cellSelectionHandleBorderColor`
| Border color of selection handles | |
CSS: `--ht-cell-selection-handle-background-color`
JS: `cellSelectionHandleBackgroundColor`
| Background color of selection handles | #### Indicator variables | Variable | Description | | -------- | ------------------------------------ | |
CSS: `--ht-resize-indicator-color`
JS: `resizeIndicatorColor`
| Color of resize indicators | |
CSS: `--ht-move-backlight-color`
JS: `moveBacklightColor`
| Background color for move operations | |
CSS: `--ht-move-backlight-opacity`
JS: `moveBacklightOpacity`
| Opacity of move backlight | |
CSS: `--ht-move-indicator-color`
JS: `moveIndicatorColor`
| Color of move indicators | |
CSS: `--ht-hidden-indicator-color`
JS: `hiddenIndicatorColor`
| Color of hidden element indicators | #### Scrollbar variables | Variable | Description | | -------- | ------------------------------------ | |
CSS: `--ht-scrollbar-border-radius`
JS: `scrollbarBorderRadius`
| Border radius of scrollbars | |
CSS: `--ht-scrollbar-track-color`
JS: `scrollbarTrackColor`
| Background color of scrollbar tracks | |
CSS: `--ht-scrollbar-thumb-color`
JS: `scrollbarThumbColor`
| Color of scrollbar thumbs | #### Header variables | Variable | Description | | -------- | ---------------------------------------- | |
CSS: `--ht-header-font-weight`
JS: `headerFontWeight`
| Font weight for header text | |
CSS: `--ht-header-foreground-color`
JS: `headerForegroundColor`
| Text color for headers | |
CSS: `--ht-header-background-color`
JS: `headerBackgroundColor`
| Background color for headers | |
CSS: `--ht-header-highlighted-shadow-size`
JS: `headerHighlightedShadowSize`
| Shadow size for highlighted headers | |
CSS: `--ht-header-highlighted-foreground-color`
JS: `headerHighlightedForegroundColor`
| Text color for highlighted headers | |
CSS: `--ht-header-highlighted-background-color`
JS: `headerHighlightedBackgroundColor`
| Background color for highlighted headers | |
CSS: `--ht-header-active-border-color`
JS: `headerActiveBorderColor`
| Border color for active headers | |
CSS: `--ht-header-active-foreground-color`
JS: `headerActiveForegroundColor`
| Text color for active headers | |
CSS: `--ht-header-active-background-color`
JS: `headerActiveBackgroundColor`
| Background color for active headers | |
CSS: `--ht-header-filter-background-color`
JS: `headerFilterBackgroundColor`
| Background color for header filters | #### Header row variables | Variable | Description | | -------- | -------------------------------------------- | |
CSS: `--ht-header-row-foreground-color`
JS: `headerRowForegroundColor`
| Text color for header rows | |
CSS: `--ht-header-row-background-color`
JS: `headerRowBackgroundColor`
| Background color for header rows | |
CSS: `--ht-header-row-highlighted-foreground-color`
JS: `headerRowHighlightedForegroundColor`
| Text color for highlighted header rows | |
CSS: `--ht-header-row-highlighted-background-color`
JS: `headerRowHighlightedBackgroundColor`
| Background color for highlighted header rows | |
CSS: `--ht-header-row-active-foreground-color`
JS: `headerRowActiveForegroundColor`
| Text color for active header rows | |
CSS: `--ht-header-row-active-background-color`
JS: `headerRowActiveBackgroundColor`
| Background color for active header rows | #### Checkbox variables | Variable | Description | | -------- | ----------------------------------------------------- | |
CSS: `--ht-checkbox-size`
JS: `checkboxSize`
| Size of checkbox elements | |
CSS: `--ht-checkbox-border-radius`
JS: `checkboxBorderRadius`
| Border radius of checkboxes | |
CSS: `--ht-checkbox-border-color`
JS: `checkboxBorderColor`
| Border color of checkboxes | |
CSS: `--ht-checkbox-background-color`
JS: `checkboxBackgroundColor`
| Background color of checkboxes | |
CSS: `--ht-checkbox-icon-color`
JS: `checkboxIconColor`
| Color of checkbox icons | |
CSS: `--ht-checkbox-focus-border-color`
JS: `checkboxFocusBorderColor`
| Border color of focused checkboxes | |
CSS: `--ht-checkbox-focus-background-color`
JS: `checkboxFocusBackgroundColor`
| Background color of focused checkboxes | |
CSS: `--ht-checkbox-focus-icon-color`
JS: `checkboxFocusIconColor`
| Icon color of focused checkboxes | |
CSS: `--ht-checkbox-focus-ring-color`
JS: `checkboxFocusRingColor`
| Focus ring color for checkboxes | |
CSS: `--ht-checkbox-disabled-border-color`
JS: `checkboxDisabledBorderColor`
| Border color of disabled checkboxes | |
CSS: `--ht-checkbox-disabled-background-color`
JS: `checkboxDisabledBackgroundColor`
| Background color of disabled checkboxes | |
CSS: `--ht-checkbox-disabled-icon-color`
JS: `checkboxDisabledIconColor`
| Icon color of disabled checkboxes | |
CSS: `--ht-checkbox-checked-border-color`
JS: `checkboxCheckedBorderColor`
| Border color of checked checkboxes | |
CSS: `--ht-checkbox-checked-background-color`
JS: `checkboxCheckedBackgroundColor`
| Background color of checked checkboxes | |
CSS: `--ht-checkbox-checked-icon-color`
JS: `checkboxCheckedIconColor`
| Icon color of checked checkboxes | |
CSS: `--ht-checkbox-checked-focus-border-color`
JS: `checkboxCheckedFocusBorderColor`
| Border color of focused checked checkboxes | |
CSS: `--ht-checkbox-checked-focus-background-color`
JS: `checkboxCheckedFocusBackgroundColor`
| Background color of focused checked checkboxes | |
CSS: `--ht-checkbox-checked-focus-icon-color`
JS: `checkboxCheckedFocusIconColor`
| Icon color of focused checked checkboxes | |
CSS: `--ht-checkbox-checked-disabled-border-color`
JS: `checkboxCheckedDisabledBorderColor`
| Border color of disabled checked checkboxes | |
CSS: `--ht-checkbox-checked-disabled-background-color`
JS: `checkboxCheckedDisabledBackgroundColor`
| Background color of disabled checked checkboxes | |
CSS: `--ht-checkbox-checked-disabled-icon-color`
JS: `checkboxCheckedDisabledIconColor`
| Icon color of disabled checked checkboxes | |
CSS: `--ht-checkbox-indeterminate-border-color`
JS: `checkboxIndeterminateBorderColor`
| Border color of indeterminate checkboxes | |
CSS: `--ht-checkbox-indeterminate-background-color`
JS: `checkboxIndeterminateBackgroundColor`
| Background color of indeterminate checkboxes | |
CSS: `--ht-checkbox-indeterminate-icon-color`
JS: `checkboxIndeterminateIconColor`
| Icon color of indeterminate checkboxes | |
CSS: `--ht-checkbox-indeterminate-focus-border-color`
JS: `checkboxIndeterminateFocusBorderColor`
| Border color of focused indeterminate checkboxes | |
CSS: `--ht-checkbox-indeterminate-focus-background-color`
JS: `checkboxIndeterminateFocusBackgroundColor`
| Background color of focused indeterminate checkboxes | |
CSS: `--ht-checkbox-indeterminate-focus-icon-color`
JS: `checkboxIndeterminateFocusIconColor`
| Icon color of focused indeterminate checkboxes | |
CSS: `--ht-checkbox-indeterminate-disabled-border-color`
JS: `checkboxIndeterminateDisabledBorderColor`
| Border color of disabled indeterminate checkboxes | |
CSS: `--ht-checkbox-indeterminate-disabled-background-color`
JS: `checkboxIndeterminateDisabledBackgroundColor`
| Background color of disabled indeterminate checkboxes | |
CSS: `--ht-checkbox-indeterminate-disabled-icon-color`
JS: `checkboxIndeterminateDisabledIconColor`
| Icon color of disabled indeterminate checkboxes | #### Radio button variables | Variable | Description | | -------- | -------------------------------------------------- | |
CSS: `--ht-radio-size`
JS: `radioSize`
| Size of radio button elements | |
CSS: `--ht-radio-border-color`
JS: `radioBorderColor`
| Border color of radio buttons | |
CSS: `--ht-radio-background-color`
JS: `radioBackgroundColor`
| Background color of radio buttons | |
CSS: `--ht-radio-icon-color`
JS: `radioIconColor`
| Color of radio button icons | |
CSS: `--ht-radio-focus-border-color`
JS: `radioFocusBorderColor`
| Border color of focused radio buttons | |
CSS: `--ht-radio-focus-background-color`
JS: `radioFocusBackgroundColor`
| Background color of focused radio buttons | |
CSS: `--ht-radio-focus-icon-color`
JS: `radioFocusIconColor`
| Icon color of focused radio buttons | |
CSS: `--ht-radio-focus-ring-color`
JS: `radioFocusRingColor`
| Focus ring color for radio buttons | |
CSS: `--ht-radio-disabled-border-color`
JS: `radioDisabledBorderColor`
| Border color of disabled radio buttons | |
CSS: `--ht-radio-disabled-background-color`
JS: `radioDisabledBackgroundColor`
| Background color of disabled radio buttons | |
CSS: `--ht-radio-disabled-icon-color`
JS: `radioDisabledIconColor`
| Icon color of disabled radio buttons | |
CSS: `--ht-radio-checked-border-color`
JS: `radioCheckedBorderColor`
| Border color of checked radio buttons | |
CSS: `--ht-radio-checked-background-color`
JS: `radioCheckedBackgroundColor`
| Background color of checked radio buttons | |
CSS: `--ht-radio-checked-icon-color`
JS: `radioCheckedIconColor`
| Icon color of checked radio buttons | |
CSS: `--ht-radio-checked-focus-border-color`
JS: `radioCheckedFocusBorderColor`
| Border color of focused checked radio buttons | |
CSS: `--ht-radio-checked-focus-background-color`
JS: `radioCheckedFocusBackgroundColor`
| Background color of focused checked radio buttons | |
CSS: `--ht-radio-checked-focus-icon-color`
JS: `radioCheckedFocusIconColor`
| Icon color of focused checked radio buttons | |
CSS: `--ht-radio-checked-disabled-border-color`
JS: `radioCheckedDisabledBorderColor`
| Border color of disabled checked radio buttons | |
CSS: `--ht-radio-checked-disabled-background-color`
JS: `radioCheckedDisabledBackgroundColor`
| Background color of disabled checked radio buttons | |
CSS: `--ht-radio-checked-disabled-icon-color`
JS: `radioCheckedDisabledIconColor`
| Icon color of disabled checked radio buttons | #### Icon button variables | Variable | Description | | -------- | ----------------------------------------------- | |
CSS: `--ht-icon-button-border-radius`
JS: `iconButtonBorderRadius`
| Border radius of icon buttons | |
CSS: `--ht-icon-button-large-border-radius`
JS: `iconButtonLargeBorderRadius`
| Border radius of large icon buttons | |
CSS: `--ht-icon-button-large-padding`
JS: `iconButtonLargePadding`
| Padding of large icon buttons | |
CSS: `--ht-icon-button-border-color`
JS: `iconButtonBorderColor`
| Border color of icon buttons | |
CSS: `--ht-icon-button-background-color`
JS: `iconButtonBackgroundColor`
| Background color of icon buttons | |
CSS: `--ht-icon-button-icon-color`
JS: `iconButtonIconColor`
| Color of icon button icons | |
CSS: `--ht-icon-button-hover-border-color`
JS: `iconButtonHoverBorderColor`
| Border color of hovered icon buttons | |
CSS: `--ht-icon-button-hover-background-color`
JS: `iconButtonHoverBackgroundColor`
| Background color of hovered icon buttons | |
CSS: `--ht-icon-button-hover-icon-color`
JS: `iconButtonHoverIconColor`
| Icon color of hovered icon buttons | |
CSS: `--ht-icon-button-active-border-color`
JS: `iconButtonActiveBorderColor`
| Border color of active icon buttons | |
CSS: `--ht-icon-button-active-background-color`
JS: `iconButtonActiveBackgroundColor`
| Background color of active icon buttons | |
CSS: `--ht-icon-button-active-icon-color`
JS: `iconButtonActiveIconColor`
| Icon color of active icon buttons | |
CSS: `--ht-icon-button-active-hover-border-color`
JS: `iconButtonActiveHoverBorderColor`
| Border color of hovered active icon buttons | |
CSS: `--ht-icon-button-active-hover-background-color`
JS: `iconButtonActiveHoverBackgroundColor`
| Background color of hovered active icon buttons | |
CSS: `--ht-icon-button-active-hover-icon-color`
JS: `iconButtonActiveHoverIconColor`
| Icon color of hovered active icon buttons | |
CSS: `--ht-icon-button-hit-area-size`
JS: `iconButtonHitAreaSize`
| Hit area size of icon buttons | #### Collapse button variables | Variable | Description | | -------- | ---------------------------------------------------- | |
CSS: `--ht-collapse-button-border-radius`
JS: `collapseButtonBorderRadius`
| Border radius of collapse buttons | |
CSS: `--ht-collapse-button-open-border-color`
JS: `collapseButtonOpenBorderColor`
| Border color of open collapse buttons | |
CSS: `--ht-collapse-button-open-background-color`
JS: `collapseButtonOpenBackgroundColor`
| Background color of open collapse buttons | |
CSS: `--ht-collapse-button-open-icon-color`
JS: `collapseButtonOpenIconColor`
| Icon color of open collapse buttons | |
CSS: `--ht-collapse-button-open-icon-active-color`
JS: `collapseButtonOpenIconActiveColor`
| Active icon color of open collapse buttons | |
CSS: `--ht-collapse-button-open-hover-border-color`
JS: `collapseButtonOpenHoverBorderColor`
| Border color of hovered open collapse buttons | |
CSS: `--ht-collapse-button-open-hover-background-color`
JS: `collapseButtonOpenHoverBackgroundColor`
| Background color of hovered open collapse buttons | |
CSS: `--ht-collapse-button-open-hover-icon-color`
JS: `collapseButtonOpenHoverIconColor`
| Icon color of hovered open collapse buttons | |
CSS: `--ht-collapse-button-open-hover-icon-active-color`
JS: `collapseButtonOpenHoverIconActiveColor`
| Active icon color of hovered open collapse buttons | |
CSS: `--ht-collapse-button-close-border-color`
JS: `collapseButtonCloseBorderColor`
| Border color of closed collapse buttons | |
CSS: `--ht-collapse-button-close-background-color`
JS: `collapseButtonCloseBackgroundColor`
| Background color of closed collapse buttons | |
CSS: `--ht-collapse-button-close-icon-color`
JS: `collapseButtonCloseIconColor`
| Icon color of closed collapse buttons | |
CSS: `--ht-collapse-button-close-icon-active-color`
JS: `collapseButtonCloseIconActiveColor`
| Active icon color of closed collapse buttons | |
CSS: `--ht-collapse-button-close-hover-border-color`
JS: `collapseButtonCloseHoverBorderColor`
| Border color of hovered closed collapse buttons | |
CSS: `--ht-collapse-button-close-hover-background-color`
JS: `collapseButtonCloseHoverBackgroundColor`
| Background color of hovered closed collapse buttons | |
CSS: `--ht-collapse-button-close-hover-icon-color`
JS: `collapseButtonCloseHoverIconColor`
| Icon color of hovered closed collapse buttons | |
CSS: `--ht-collapse-button-close-hover-icon-active-color`
JS: `collapseButtonCloseHoverIconActiveColor`
| Active icon color of hovered closed collapse buttons | #### Button variables | Variable | Description | | -------- | ----------------------------- | |
CSS: `--ht-button-border-radius`
JS: `buttonBorderRadius`
| Border radius of buttons | |
CSS: `--ht-button-horizontal-padding`
JS: `buttonHorizontalPadding`
| Horizontal padding of buttons | |
CSS: `--ht-button-vertical-padding`
JS: `buttonVerticalPadding`
| Vertical padding of buttons | #### Primary button variables | Variable | Description | | -------- | -------------------------------------------- | |
CSS: `--ht-primary-button-border-color`
JS: `primaryButtonBorderColor`
| Border color of primary buttons | |
CSS: `--ht-primary-button-foreground-color`
JS: `primaryButtonForegroundColor`
| Text color of primary buttons | |
CSS: `--ht-primary-button-background-color`
JS: `primaryButtonBackgroundColor`
| Background color of primary buttons | |
CSS: `--ht-primary-button-disabled-border-color`
JS: `primaryButtonDisabledBorderColor`
| Border color of disabled primary buttons | |
CSS: `--ht-primary-button-disabled-foreground-color`
JS: `primaryButtonDisabledForegroundColor`
| Text color of disabled primary buttons | |
CSS: `--ht-primary-button-disabled-background-color`
JS: `primaryButtonDisabledBackgroundColor`
| Background color of disabled primary buttons | |
CSS: `--ht-primary-button-hover-border-color`
JS: `primaryButtonHoverBorderColor`
| Border color of hovered primary buttons | |
CSS: `--ht-primary-button-hover-foreground-color`
JS: `primaryButtonHoverForegroundColor`
| Text color of hovered primary buttons | |
CSS: `--ht-primary-button-hover-background-color`
JS: `primaryButtonHoverBackgroundColor`
| Background color of hovered primary buttons | |
CSS: `--ht-primary-button-focus-border-color`
JS: `primaryButtonFocusBorderColor`
| Border color of focused primary buttons | |
CSS: `--ht-primary-button-focus-foreground-color`
JS: `primaryButtonFocusForegroundColor`
| Text color of focused primary buttons | |
CSS: `--ht-primary-button-focus-background-color`
JS: `primaryButtonFocusBackgroundColor`
| Background color of focused primary buttons | #### Secondary button variables | Variable | Description | | -------- | ---------------------------------------------- | |
CSS: `--ht-secondary-button-border-color`
JS: `secondaryButtonBorderColor`
| Border color of secondary buttons | |
CSS: `--ht-secondary-button-foreground-color`
JS: `secondaryButtonForegroundColor`
| Text color of secondary buttons | |
CSS: `--ht-secondary-button-background-color`
JS: `secondaryButtonBackgroundColor`
| Background color of secondary buttons | |
CSS: `--ht-secondary-button-disabled-border-color`
JS: `secondaryButtonDisabledBorderColor`
| Border color of disabled secondary buttons | |
CSS: `--ht-secondary-button-disabled-foreground-color`
JS: `secondaryButtonDisabledForegroundColor`
| Text color of disabled secondary buttons | |
CSS: `--ht-secondary-button-disabled-background-color`
JS: `secondaryButtonDisabledBackgroundColor`
| Background color of disabled secondary buttons | |
CSS: `--ht-secondary-button-hover-border-color`
JS: `secondaryButtonHoverBorderColor`
| Border color of hovered secondary buttons | |
CSS: `--ht-secondary-button-hover-foreground-color`
JS: `secondaryButtonHoverForegroundColor`
| Text color of hovered secondary buttons | |
CSS: `--ht-secondary-button-hover-background-color`
JS: `secondaryButtonHoverBackgroundColor`
| Background color of hovered secondary buttons | |
CSS: `--ht-secondary-button-focus-border-color`
JS: `secondaryButtonFocusBorderColor`
| Border color of focused secondary buttons | |
CSS: `--ht-secondary-button-focus-foreground-color`
JS: `secondaryButtonFocusForegroundColor`
| Text color of focused secondary buttons | |
CSS: `--ht-secondary-button-focus-background-color`
JS: `secondaryButtonFocusBackgroundColor`
| Background color of focused secondary buttons | #### Chip variables | Variable | Description | | -------- | ----------------------------------- | |
CSS: `--ht-chip-background`
JS: `chipBackground`
| Background color of chip elements | |
CSS: `--ht-chip-border-radius`
JS: `chipBorderRadius`
| Border radius of chip elements | |
CSS: `--ht-chip-vertical-padding`
JS: `chipVerticalPadding`
| Vertical padding of chip elements | |
CSS: `--ht-chip-horizontal-padding`
JS: `chipHorizontalPadding`
| Horizontal padding of chip elements | |
CSS: `--ht-chip-gap`
JS: `chipGap`
| Gap between chip elements | #### Comments variables | Variable | Description | | -------- | --------------------------------------------- | |
CSS: `--ht-comments-textarea-horizontal-padding`
JS: `commentsTextareaHorizontalPadding`
| Horizontal padding of comment textareas | |
CSS: `--ht-comments-textarea-vertical-padding`
JS: `commentsTextareaVerticalPadding`
| Vertical padding of comment textareas | |
CSS: `--ht-comments-textarea-border-width`
JS: `commentsTextareaBorderWidth`
| Border width of comment textareas | |
CSS: `--ht-comments-textarea-border-color`
JS: `commentsTextareaBorderColor`
| Border color of comment textareas | |
CSS: `--ht-comments-textarea-foreground-color`
JS: `commentsTextareaForegroundColor`
| Text color of comment textareas | |
CSS: `--ht-comments-textarea-background-color`
JS: `commentsTextareaBackgroundColor`
| Background color of comment textareas | |
CSS: `--ht-comments-textarea-focus-border-width`
JS: `commentsTextareaFocusBorderWidth`
| Border width of focused comment textareas | |
CSS: `--ht-comments-textarea-focus-border-color`
JS: `commentsTextareaFocusBorderColor`
| Border color of focused comment textareas | |
CSS: `--ht-comments-textarea-focus-foreground-color`
JS: `commentsTextareaFocusForegroundColor`
| Text color of focused comment textareas | |
CSS: `--ht-comments-textarea-focus-background-color`
JS: `commentsTextareaFocusBackgroundColor`
| Background color of focused comment textareas | |
CSS: `--ht-comments-indicator-size`
JS: `commentsIndicatorSize`
| Size of comment indicators | |
CSS: `--ht-comments-indicator-color`
JS: `commentsIndicatorColor`
| Color of comment indicators | #### License variables | Variable | Description | | -------- | -------------------------------------- | |
CSS: `--ht-license-horizontal-padding`
JS: `licenseHorizontalPadding`
| Horizontal padding of license elements | |
CSS: `--ht-license-vertical-padding`
JS: `licenseVerticalPadding`
| Vertical padding of license elements | |
CSS: `--ht-license-foreground-color`
JS: `licenseForegroundColor`
| Text color of license elements | |
CSS: `--ht-license-background-color`
JS: `licenseBackgroundColor`
| Background color of license elements | #### Link variables | Variable | Description | | -------- | ---------------------- | |
CSS: `--ht-link-color`
JS: `linkColor`
| Color of links | |
CSS: `--ht-link-hover-color`
JS: `linkHoverColor`
| Color of hovered links | #### Input variables | Variable | Description | | -------- | ------------------------------------------- | |
CSS: `--ht-input-border-width`
JS: `inputBorderWidth`
| Border width of input elements | |
CSS: `--ht-input-border-radius`
JS: `inputBorderRadius`
| Border radius of input elements | |
CSS: `--ht-input-horizontal-padding`
JS: `inputHorizontalPadding`
| Horizontal padding of input elements | |
CSS: `--ht-input-vertical-padding`
JS: `inputVerticalPadding`
| Vertical padding of input elements | |
CSS: `--ht-input-border-color`
JS: `inputBorderColor`
| Border color of input elements | |
CSS: `--ht-input-foreground-color`
JS: `inputForegroundColor`
| Text color of input elements | |
CSS: `--ht-input-background-color`
JS: `inputBackgroundColor`
| Background color of input elements | |
CSS: `--ht-input-hover-border-color`
JS: `inputHoverBorderColor`
| Border color of hovered input elements | |
CSS: `--ht-input-hover-foreground-color`
JS: `inputHoverForegroundColor`
| Text color of hovered input elements | |
CSS: `--ht-input-hover-background-color`
JS: `inputHoverBackgroundColor`
| Background color of hovered input elements | |
CSS: `--ht-input-disabled-border-color`
JS: `inputDisabledBorderColor`
| Border color of disabled input elements | |
CSS: `--ht-input-disabled-foreground-color`
JS: `inputDisabledForegroundColor`
| Text color of disabled input elements | |
CSS: `--ht-input-disabled-background-color`
JS: `inputDisabledBackgroundColor`
| Background color of disabled input elements | |
CSS: `--ht-input-focus-border-color`
JS: `inputFocusBorderColor`
| Border color of focused input elements | |
CSS: `--ht-input-focus-foreground-color`
JS: `inputFocusForegroundColor`
| Text color of focused input elements | |
CSS: `--ht-input-focus-background-color`
JS: `inputFocusBackgroundColor`
| Background color of focused input elements | #### Menu variables | Variable | Description | | -------- | --------------------------------------- | |
CSS: `--ht-menu-border-width`
JS: `menuBorderWidth`
| Border width of menu elements | |
CSS: `--ht-menu-border-radius`
JS: `menuBorderRadius`
| Border radius of menu elements | |
CSS: `--ht-menu-horizontal-padding`
JS: `menuHorizontalPadding`
| Horizontal padding of menu elements | |
CSS: `--ht-menu-vertical-padding`
JS: `menuVerticalPadding`
| Vertical padding of menu elements | |
CSS: `--ht-menu-item-horizontal-padding`
JS: `menuItemHorizontalPadding`
| Horizontal padding of menu items | |
CSS: `--ht-menu-item-vertical-padding`
JS: `menuItemVerticalPadding`
| Vertical padding of menu items | |
CSS: `--ht-menu-border-color`
JS: `menuBorderColor`
| Border color of menu elements | |
CSS: `--ht-menu-shadow-x`
JS: `menuShadowX`
| Horizontal shadow offset of menus | |
CSS: `--ht-menu-shadow-y`
JS: `menuShadowY`
| Vertical shadow offset of menus | |
CSS: `--ht-menu-shadow-blur`
JS: `menuShadowBlur`
| Shadow blur radius of menus | |
CSS: `--ht-menu-shadow-color`
JS: `menuShadowColor`
| Shadow color of menus | |
CSS: `--ht-menu-shadow-opacity`
JS: `menuShadowOpacity`
| Shadow opacity of menus | |
CSS: `--ht-menu-item-hover-color`
JS: `menuItemHoverColor`
| Background color of hovered menu items | |
CSS: `--ht-menu-item-hover-color-opacity`
JS: `menuItemHoverColorOpacity`
| Opacity of hovered menu item background | |
CSS: `--ht-menu-item-active-color`
JS: `menuItemActiveColor`
| Background color of active menu items | |
CSS: `--ht-menu-item-active-color-opacity`
JS: `menuItemActiveColorOpacity`
| Opacity of active menu item background | #### Dialog variables | Variable | Description | | -------- | --------------------------------------------------- | |
CSS: `--ht-dialog-semi-transparent-background-color`
JS: `dialogSemiTransparentBackgroundColor`
| Semi-transparent background color of dialog overlay | |
CSS: `--ht-dialog-semi-transparent-background-opacity`
JS: `dialogSemiTransparentBackgroundOpacity`
| Opacity of semi-transparent dialog background | |
CSS: `--ht-dialog-solid-background-color`
JS: `dialogSolidBackgroundColor`
| Solid background color of dialog overlay | |
CSS: `--ht-dialog-content-padding-horizontal`
JS: `dialogContentPaddingHorizontal`
| Horizontal padding of dialog content | |
CSS: `--ht-dialog-content-padding-vertical`
JS: `dialogContentPaddingVertical`
| Vertical padding of dialog content | |
CSS: `--ht-dialog-content-border-radius`
JS: `dialogContentBorderRadius`
| Border radius of dialog content | |
CSS: `--ht-dialog-content-background-color`
JS: `dialogContentBackgroundColor`
| Background color of dialog content | #### Notification variables These variables style the [Notification](@/guides/dialog/notification/notification.md) plugin toasts. Shared layout tokens (for example `borderRadius`, `tableTransition`, `gapSize`) and icon-button tokens still apply to the close control and spacing. | Variable | Description | | -------- | --------------------------------------------------- | |
CSS: `--ht-notification-foreground-color`
JS: `notificationForegroundColor`
| Text color of notification toasts | |
CSS: `--ht-notification-background-color`
JS: `notificationBackgroundColor`
| Background color of notification toasts | |
CSS: `--ht-notification-border-color`
JS: `notificationBorderColor`
| Border color of notification toasts | |
CSS: `--ht-notification-success-accent`
JS: `notificationSuccessAccent`
| Accent bar color for success toasts | |
CSS: `--ht-notification-warning-accent`
JS: `notificationWarningAccent`
| Accent bar color for warning toasts | |
CSS: `--ht-notification-error-accent`
JS: `notificationErrorAccent`
| Accent bar color for error toasts | #### Pagination variables | Variable | Description | | -------- | ------------------------------------ | |
CSS: `--ht-pagination-bar-foreground-color`
JS: `paginationBarForegroundColor`
| Text color of pagination bar | |
CSS: `--ht-pagination-bar-background-color`
JS: `paginationBarBackgroundColor`
| Background color of pagination bar | |
CSS: `--ht-pagination-bar-horizontal-padding`
JS: `paginationBarHorizontalPadding`
| Horizontal padding of pagination bar | |
CSS: `--ht-pagination-bar-vertical-padding`
JS: `paginationBarVerticalPadding`
| Vertical padding of pagination bar | |
CSS: `--ht-pagination-button-border-color`
JS: `paginationButtonBorderColor`
| Border color of pagination navigation button | |
CSS: `--ht-pagination-button-foreground-color`
JS: `paginationButtonForegroundColor`
| Icon color of pagination navigation button | |
CSS: `--ht-pagination-button-background-color`
JS: `paginationButtonBackgroundColor`
| Background color of pagination navigation button | |
CSS: `--ht-pagination-button-hover-border-color`
JS: `paginationButtonHoverBorderColor`
| Border color of pagination navigation button on hover | |
CSS: `--ht-pagination-button-hover-foreground-color`
JS: `paginationButtonHoverForegroundColor`
| Icon color of pagination navigation button on hover | |
CSS: `--ht-pagination-button-hover-background-color`
JS: `paginationButtonHoverBackgroundColor`
| Background color of pagination navigation button on hover | |
CSS: `--ht-pagination-button-disabled-border-color`
JS: `paginationButtonDisabledBorderColor`
| Border color of pagination navigation button when disabled | |
CSS: `--ht-pagination-button-disabled-foreground-color`
JS: `paginationButtonDisabledForegroundColor`
| Icon color of pagination navigation button when disabled | |
CSS: `--ht-pagination-button-disabled-background-color`
JS: `paginationButtonDisabledBackgroundColor`
| Background color of pagination navigation button when disabled | |
CSS: `--ht-pagination-button-focus-border-color`
JS: `paginationButtonFocusBorderColor`
| Border color of pagination navigation button on focus | |
CSS: `--ht-pagination-button-focus-foreground-color`
JS: `paginationButtonFocusForegroundColor`
| Icon color of pagination navigation button on focus | |
CSS: `--ht-pagination-button-focus-background-color`
JS: `paginationButtonFocusBackgroundColor`
| Background color of pagination navigation button on focus |
## Related blog articles
- [From components to tables: Designing a data table component in your design system](https://handsontable.com/blog/from-components-to-tables-designing-a-data-table-component-in-your-design-system) - [Handsontable 14.6.0: Easier styling and enhanced undo-redo](https://handsontable.com/blog/handsontable-14-6-0-easier-styling-and-enhanced-undo-redo)
--- # Legacy style URL: https://handsontable.com/docs/javascript-data-grid/legacy-style/ Starting from **version 17.0.0**, the legacy stylesheet has been removed from Handsontable. If you're upgrading from an earlier version, you must migrate to the **Classic** theme. [[toc]] ## Legacy styles are no longer available The legacy CSS file (`handsontable.full.min.css`) was the default stylesheet up until **version 15** (released in December 2024). In version 16.1, Handsontable introduced a new theming system with the **Classic** theme as a replacement. As of **version 17.0.0**, the legacy stylesheet has been completely removed. If you're upgrading from a version prior to 17.0.0, you must migrate to the Classic theme to ensure your grid displays correctly. ## Migrate to the Classic theme There are two ways to apply the Classic theme. The recommended approach is to use the Theme API with a theme object. ### Option 1: Using the Theme API (recommended) The Theme API allows you to import and register themes programmatically. This approach provides full access to theme customization features like density modes and color schemes. ::: only-for javascript ```js import Handsontable from 'handsontable'; import { classicTheme } from 'handsontable/themes'; const hot = new Handsontable(container, { theme: classicTheme, // ... other options }); ``` ::: ::: only-for react ```jsx import { HotTable } from '@handsontable/react-wrapper'; import { classicTheme } from 'handsontable/themes'; function App() { return ( ); } ``` ::: ::: only-for angular ```ts import { classicTheme } from 'handsontable/themes'; // In your component @Component({ template: `` }) export class AppComponent { hotSettings = { theme: classicTheme, // ... other options }; } ``` ::: ::: only-for vue ```ts import { ref } from 'vue'; import { HotTable } from '@handsontable/vue3'; import { registerAllModules } from 'handsontable/registry'; import { classicTheme } from 'handsontable/themes'; registerAllModules(); const hotSettings = ref({ theme: classicTheme, // ... other options licenseKey: 'non-commercial-and-evaluation', }); ``` ```html ``` ::: ### Option 2: Using CSS files with theme as string Alternatively, you can use CSS files and pass the theme name as a string to the `theme` option. #### Step 1. Update your CSS imports Replace your existing CSS import with the base styles and Classic theme: ```diff - @import 'handsontable/dist/handsontable.full.min.css'; + @import 'handsontable/styles/ht-theme-classic.min.css'; ``` Or if you're using JavaScript imports: ```diff - import 'handsontable/dist/handsontable.full.min.css'; + import 'handsontable/styles/ht-theme-classic.min.css'; ``` #### Step 2. Set the theme in Handsontable configuration ::: only-for javascript ```js const hot = new Handsontable(container, { theme: 'ht-theme-classic', // ... other options }); ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```html ``` ::: ::: only-for vue ```ts const hotSettings = ref({ theme: 'ht-theme-classic', // ... other options licenseKey: 'non-commercial-and-evaluation', }); ``` ```html ``` ::: ### Why migrate to Classic? The Classic theme provides the same visual appearance as the legacy style, but with significant improvements: - **CSS variables**: Easily customize colors, spacing, and other visual properties - **Better maintainability**: The theming system is designed for long-term support - **Consistency**: Works seamlessly with the new Design System **Related guides**
- [Migrate from 16.2 to 17.0](@/guides/upgrade-and-migration/migrating-from-16.2-to-17.0/migrating-from-16.2-to-17.0.md) - [Themes](@/guides/styling/themes/themes.md)
## Result Your grid now uses the legacy stylesheet. The visual style matches pre-14.0 Handsontable. --- # Column headers URL: https://handsontable.com/docs/javascript-data-grid/column-header/ Use default column headers (A, B, C), or set them to custom values provided by an array or a function. [[toc]] ## Overview Column headers are gray-colored rows used to label each column or group of columns. By default, these headers are populated with letters in alphabetical order. To reflect the type or category of data in a particular column, give it a custom name and then display it in a column header. For example, instead of letters as labels such as `A, B, C, ...` name them `ID, Full name, Country, ...`. ## Default headers Setting the [`colHeaders`](@/api/options.md#colheaders) option to `true` enables the default column headers as shown in the example below: ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/columns/column-header/javascript/example1.js) @[code](@/content/guides/columns/column-header/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-header/react/example1.jsx) @[code](@/content/guides/columns/column-header/react/example1.tsx) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/columns/column-header/vue/example1.vue) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-header/angular/example1.ts) @[code](@/content/guides/columns/column-header/angular/example1.html) ::: ::: ## Header labels as an array An array of labels can be used to set the [`colHeaders`](@/api/options.md#colheaders) as shown in the example below: ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/columns/column-header/javascript/example2.js) @[code](@/content/guides/columns/column-header/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-header/react/example2.jsx) @[code](@/content/guides/columns/column-header/react/example2.tsx) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/columns/column-header/vue/example2.vue) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-header/angular/example2.ts) @[code](@/content/guides/columns/column-header/angular/example2.html) ::: ::: ## Header labels as a function The [`colHeaders`](@/api/options.md#colheaders) can also be populated using a function as shown in the example below: ::: only-for javascript ::: example #example3 --js 1 --ts 2 @[code](@/content/guides/columns/column-header/javascript/example3.js) @[code](@/content/guides/columns/column-header/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-header/react/example3.jsx) @[code](@/content/guides/columns/column-header/react/example3.tsx) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/columns/column-header/vue/example3.vue) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-header/angular/example3.ts) @[code](@/content/guides/columns/column-header/angular/example3.html) ::: ::: ## Header labels in the columns option When you configure columns individually with the [`columns`](@/api/options.md#columns) option, set a column's header label with that column's [`title`](@/api/options.md#title) option. If both are set, a column's `title` takes precedence over the matching [`colHeaders`](@/api/options.md#colheaders) entry. ::: only-for javascript ::: example #example6 --js 1 --ts 2 @[code](@/content/guides/columns/column-header/javascript/example6.js) @[code](@/content/guides/columns/column-header/javascript/example6.ts) ::: ::: ::: only-for react ::: example #example6 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-header/react/example6.jsx) @[code](@/content/guides/columns/column-header/react/example6.tsx) ::: ::: ::: only-for vue ::: example #example6 :vue3 @[code](@/content/guides/columns/column-header/vue/example6.vue) ::: ::: ::: only-for angular ::: example #example6 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-header/angular/example6.ts) @[code](@/content/guides/columns/column-header/angular/example6.html) ::: ::: ## Customize column headers You can align the text in the header label with the [`headerClassName`](@/api/options.md#headerclassname) option. Setting it to `htLeft`, `htCenter`, or `htRight` will align the header labels to the left, center, or right, respectively. You can also set the alignment for a specific column by using the [`columns`](@/api/options.md#columns) option. ::: only-for javascript ::: example #example4 --js 1 --ts 2 @[code](@/content/guides/columns/column-header/javascript/example4.js) @[code](@/content/guides/columns/column-header/javascript/example4.ts) ::: ::: ::: only-for react ::: example #example4 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-header/react/example4.jsx) @[code](@/content/guides/columns/column-header/react/example4.tsx) ::: ::: ::: only-for vue ::: example #example4 :vue3 @[code](@/content/guides/columns/column-header/vue/example4.vue) ::: ::: ::: only-for angular ::: example #example4 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-header/angular/example4.ts) @[code](@/content/guides/columns/column-header/angular/example4.html) ::: ::: If you want to style the header labels, you can pass any number of class names, separated by a space, to the [`headerClassName`](@/api/options.md#headerclassname) option. ::: only-for javascript ::: example #example5 --css 1 --js 2 --ts 3 @[code](@/content/guides/columns/column-header/javascript/example5.css) @[code](@/content/guides/columns/column-header/javascript/example5.js) @[code](@/content/guides/columns/column-header/javascript/example5.ts) ::: ::: ::: only-for react ::: example #example5 :react --css 1 --js 2 --ts 3 @[code](@/content/guides/columns/column-header/react/example5.css) @[code](@/content/guides/columns/column-header/react/example5.jsx) @[code](@/content/guides/columns/column-header/react/example5.tsx) ::: ::: ::: only-for vue ::: example #example5 :vue3 @[code](@/content/guides/columns/column-header/vue/example5.vue) ::: ::: ::: only-for angular ::: example #example5 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-header/angular/example5.ts) @[code](@/content/guides/columns/column-header/angular/example5.html) ::: ::: ## Column header height When column labels are longer, header text can wrap and require more vertical space. To control the header size, set [`columnHeaderHeight`](@/api/options.md#columnheaderheight). You can set this option to one of the following: - A number - set the same height for every column header. - An array - set different heights for individual column header levels. The example below uses longer labels together with `columnHeaderHeight: 50`. ::: only-for javascript ::: example #example7 --js 1 --ts 2 @[code](@/content/guides/columns/column-header/javascript/example7.js) @[code](@/content/guides/columns/column-header/javascript/example7.ts) ::: ::: ::: only-for react ::: example #example7 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-header/react/example7.jsx) @[code](@/content/guides/columns/column-header/react/example7.tsx) ::: ::: ::: only-for vue ::: example #example7 :vue3 @[code](@/content/guides/columns/column-header/vue/example7.vue) ::: ::: ::: only-for angular ::: example #example7 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-header/angular/example7.ts) @[code](@/content/guides/columns/column-header/angular/example7.html) ::: ::: ## Nested headers More complex data structures can be displayed with multiple headers, each representing a different category of data. To learn more about nested headers, see the [column groups](@/guides/columns/column-groups/column-groups.md) page. ## Related articles **Related guides**
- [Column groups](@/guides/columns/column-groups/column-groups.md)
**Related blog articles**
- [Handsontable 14.5.0: Improved performance and flexible column header class](https://handsontable.com/blog/handsontable-14.5.0-improved-performance-and-flexible-column-header-class)
**Configuration options**
- [activeHeaderClassName](@/api/options.md#activeheaderclassname) - [colHeaders](@/api/options.md#colheaders) - [columnHeaderHeight](@/api/options.md#columnheaderheight) - [currentHeaderClassName](@/api/options.md#currentheaderclassname) - [nestedHeaders](@/api/options.md#nestedheaders) - [title](@/api/options.md#title)
**Core methods**
- [getColHeader()](@/api/core.md#getcolheader) - [hasColHeaders()](@/api/core.md#hascolheaders)
**Hooks**
- [afterGetColHeader](@/api/hooks.md#aftergetcolheader) - [afterGetColumnHeaderRenderers](@/api/hooks.md#aftergetcolumnheaderrenderers) - [beforeHighlightingColumnHeader](@/api/hooks.md#beforehighlightingcolumnheader) - [modifyColHeader](@/api/hooks.md#modifycolheader) - [modifyColumnHeaderHeight](@/api/hooks.md#modifycolumnheaderheight)
**Plugins**
- [NestedHeaders](@/api/nestedHeaders.md)
--- # Column groups URL: https://handsontable.com/docs/javascript-data-grid/column-groups/ Group your columns, using multiple levels of nested column headers, to better reflect the structure of your data. [[toc]] ## Nested column headers The [`NestedHeaders`](@/api/nestedHeaders.md) plugin allows you to create a nested headers structure by using the HTML `colspan` and `rowspan` attributes. To create a header that spans multiple columns, its corresponding configuration array element should be provided as an object with `label` and `colspan` properties. The `label` property defines the header's label, while the `colspan` property defines the number of columns that the header should cover. To create a header that spans multiple header rows, add a `rowspan` property to that object. See [Rowspan](#rowspan) below. ### Header configuration object Each `nestedHeaders` entry is either a string label or an object with these properties: | Property | Type | Description | | ----------------- | -------- | ----------- | | `label` | `string` | The header's text. | | `colspan` | `number` | Columns the header spans (an integer greater than `1`); groups the columns it covers. | | `rowspan` | `number` | Header rows the header spans (an integer greater than `1`). See [Rowspan](#rowspan). | | `headerClassName` | `string` | One or more space-separated CSS class names added to the header element (for example, `'htRight'`). | | `visibleWhen` | `string` | For a header in a collapsible group, which collapse state keeps it visible: `'collapsed'`, `'expanded'`, or `'always'`. See [Choose which columns stay visible when collapsed](#choose-which-columns-stay-visible-when-collapsed). | | `columnDropMode` | `string` | What the group does when a foreign column is moved into its span: `'adopt'` (default) or `'split'`. See [Keep a group cohesive or let it split](#keep-a-group-cohesive-or-let-it-split). | | `collapsible` | `boolean` | Whether the group can be collapsed and expanded. You enable it through the [`collapsibleColumns`](@/api/options.md#collapsiblecolumns) option, not in the `nestedHeaders` object. See [Collapsible headers](#collapsible-headers). | For defaults and full details, see the [`nestedHeaders` API reference](@/api/options.md#nestedheaders). ::: warning A `label` is written to the DOM as HTML, so a label built from user input or an external system can inject markup. Handsontable does not sanitize it by default. Set the [`sanitizer`](@/api/options.md#sanitizer) option, which receives nested header labels under the `'header'` source. See [Content sanitizing](@/guides/security/security/security.md#content-sanitizing). ::: ### Configuration ::: only-for javascript ```js nestedHeaders: [ ['A', { label: 'B', colspan: 8 }, 'C'], ['D', { label: 'E', colspan: 4 }, { label: 'F', colspan: 4 }, 'G'], ['H', { label: 'I', colspan: 2 }, { label: 'J', colspan: 2 }, { label: 'K', colspan: 2 }, { label: 'L', colspan: 2 }, 'M'], ['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W'], ]; ``` ::: ::: only-for react ```jsx nestedHeaders={[ ['A', { label: 'B', colspan: 8 }, 'C'], ['D', { label: 'E', colspan: 4 }, { label: 'F', colspan: 4 }, 'G'], ['H', { label: 'I', colspan: 2 }, { label: 'J', colspan: 2 }, { label: 'K', colspan: 2 }, { label: 'L', colspan: 2 }, 'M'], ['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W'] ]} ``` ::: ::: only-for vue ```js nestedHeaders: [ ['A', { label: 'B', colspan: 8 }, 'C'], ['D', { label: 'E', colspan: 4 }, { label: 'F', colspan: 4 }, 'G'], ['H', { label: 'I', colspan: 2 }, { label: 'J', colspan: 2 }, { label: 'K', colspan: 2 }, { label: 'L', colspan: 2 }, 'M'], ['N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W'], ], ``` ::: ### Example ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/columns/column-groups/javascript/example1.js) @[code](@/content/guides/columns/column-groups/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-groups/react/example1.jsx) @[code](@/content/guides/columns/column-groups/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-groups/angular/example1.ts) @[code](@/content/guides/columns/column-groups/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/columns/column-groups/vue/example1.vue) ::: ::: ### Rowspan The `rowspan` property sets how many header rows a single header cell should cover. Use an integer greater than `1`. Positions in lower rows that sit under that cell can use an empty string `''` as a placeholder, but those placeholders are optional. Handsontable can infer covered slots when you omit them. You can combine `rowspan` and `colspan` on the same header object. The same rules apply as for colspan only: a header cannot be wider than its parent in the hierarchy, and overlapping header definitions are not supported. #### Configuration ::: only-for javascript ```js nestedHeaders: [ [{ label: 'A', rowspan: 2 }, { label: 'B', colspan: 2 }], ['', 'C', 'D'], ]; ``` ::: ::: only-for react ```jsx nestedHeaders={[ [{ label: 'A', rowspan: 2 }, { label: 'B', colspan: 2 }], ['', 'C', 'D'], ]} ``` ::: ::: only-for angular ```ts nestedHeaders: [ [{ label: 'A', rowspan: 2 }, { label: 'B', colspan: 2 }], ['', 'C', 'D'], ]; ``` ::: ::: only-for vue ```js nestedHeaders: [ [{ label: 'A', rowspan: 2 }, { label: 'B', colspan: 2 }], ['', 'C', 'D'], ], ``` ::: ## Collapsible headers The [`CollapsibleColumns`](@/api/collapsibleColumns.md) plugin enables columns and their headers to be collapsed/expanded. This plugin adds multi-column headers which have buttons. Clicking these buttons will collapse or expand all "child" headers, leaving the first one visible. The [`NestedHeaders`](@/api/nestedHeaders.md) plugin needs to be enabled for this to work properly. ### Configuration To enable the Collapsible Columns plugin, either set the [`collapsibleColumns`](@/api/options.md#collapsiblecolumns) configuration option to: - `true` - this will enable the functionality for _all_ multi-column headers, every column with the `colspan` attribute defined will be extended with the "expand/collapse" button - An array of objects containing information specifying which headers should have the "expand/collapse" buttons for example: ::: only-for javascript ```js collapsibleColumns: [ { row: -4, col: 1, collapsible: true }, // Add the button to the 4th-level header of the 1st column - counting from the first table row upwards. { row: -3, col: 5, collapsible: true }, // Add the button to the 3rd-level header of the 5th column - counting from the first table row upwards. ]; ``` ::: ::: only-for react ```jsx collapsibleColumns={[ { row: -4, col: 1, collapsible: true }, // Add the button to the 4th-level header of the 1st column - counting from the first table row upwards. { row: -3, col: 5, collapsible: true } // Add the button to the 3rd-level header of the 5th column - counting from the first table row upwards. ]} ``` ::: ::: only-for angular ```js collapsibleColumns: [ { row: -4, col: 1, collapsible: true }, // Add the button to the 4th-level header of the 1st column - counting from the first table row upwards. { row: -3, col: 5, collapsible: true }, // Add the button to the 3rd-level header of the 5th column - counting from the first table row upwards. ]; ``` ::: ::: only-for vue ```js collapsibleColumns: [ { row: -4, col: 1, collapsible: true }, // Add the button to the 4th-level header of the 1st column - counting from the first table row upwards. { row: -3, col: 5, collapsible: true }, // Add the button to the 3rd-level header of the 5th column - counting from the first table row upwards. ], ``` ::: The `row` coordinate is negative - counting from the first table row upwards - so `-1` points to the header row directly above the first table row, and `-4` points to the top header row of four nested levels. The `col` coordinate is the zero-based index of the column where the header starts. ![Diagram of the collapsibleColumns coordinate system: header-row indexes -4 to -1 counted from the first table row upwards, column indexes 0 to 6, and expand/collapse buttons at row -4, col 1 and at row -3, col 5](/img/collapsible_columns.svg) ### Example ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/columns/column-groups/javascript/example2.js) @[code](@/content/guides/columns/column-groups/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-groups/react/example2.jsx) @[code](@/content/guides/columns/column-groups/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-groups/angular/example2.ts) @[code](@/content/guides/columns/column-groups/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/columns/column-groups/vue/example2.vue) ::: ::: ### Choose which columns stay visible when collapsed By default, collapsing a group leaves its first column visible. To choose which columns stay visible in each state, add the `visibleWhen` property to a header in the `nestedHeaders` configuration. It accepts three values: - `'collapsed'` - the column is visible only while its group is collapsed (hidden while expanded). - `'expanded'` - the column is visible only while its group is expanded (hidden while collapsed). - `'always'` - the column is visible in both states. Once a group uses `visibleWhen` on any of its headers, the headers you leave unmarked default to `'expanded'` - they are hidden when the group collapses. So you only mark the column(s) you want to keep: tag one with `'always'` (stays in both states) or `'collapsed'` (a summary column that appears only on collapse). At least one column of a group always stays visible, so its collapse button is never lost. In the example below, collapse the **Q1 2025** group: the per-month columns (marked `'expanded'`) hide and the **Total** column (`'collapsed'`) appears. Expanding the group reverses it. ::: only-for javascript ::: example #example3 --js 1 --ts 2 @[code](@/content/guides/columns/column-groups/javascript/example3.js) @[code](@/content/guides/columns/column-groups/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-groups/react/example3.jsx) @[code](@/content/guides/columns/column-groups/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-groups/angular/example3.ts) @[code](@/content/guides/columns/column-groups/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/columns/column-groups/vue/example3.vue) ::: ::: A group whose headers use no `visibleWhen` markers keeps the default behavior - collapsing leaves its first column visible. The `visibleWhen` property applies only to headers within a [`collapsible`](@/api/options.md#collapsiblecolumns) group. ## Moving grouped columns When you enable the [`ManualColumnMove`](@/api/manualColumnMove.md) plugin, nested column headers follow their columns. Moving a column moves both its data and its header label, so the labels always describe the columns beneath them. When a move separates the columns of a group, that group renders as more than one header. Each header covers a contiguous run of the group's columns and repeats the group's label. No move is blocked. Moving an entire group relocates the group and its label as a single unit. The group can land between other groups. When a group is both [collapsible](#collapsible-headers) and collapsed: - A move that keeps the group's columns together keeps the group collapsed and moves it to its new position. - A move that would separate the group's columns expands the group first, so none of its columns stay hidden without a way to expand them. ### Keep a group cohesive or let it split By default, a group is _cohesive_. When you move a column into the group's span, the group adopts that column as a member and stays a single header that grows to cover it. Set `columnDropMode: 'split'` on the group's header object to change this. A group in split mode does not adopt a _foreign_ column - one that belongs to a different group - moved into its span. It keeps its identity and renders as several same-label banners around that column. `columnDropMode` controls only whether a group absorbs columns from outside it. A group always reclaims its own columns: moving one of a group's columns out and then back into the group's span merges it into a single banner again, regardless of its `columnDropMode`. Moving a column _out of_ a group separates a group in either mode the same way: the group renders as more than one header, each repeating its label. ::: only-for javascript ```js nestedHeaders: [ // "Q1 2025" is cohesive (the default); "Q2 2025" splits when a column moves into it. [{ label: 'Q1 2025 (adopt mode)', colspan: 3 }, { label: 'Q2 2025 (split mode)', colspan: 3, columnDropMode: 'split' }], ['January', 'February', 'March', 'April', 'May', 'June'], ]; ``` ::: ::: only-for react ```jsx nestedHeaders={[ // "Q1 2025" is cohesive (the default); "Q2 2025" splits when a column moves into it. [{ label: 'Q1 2025 (adopt mode)', colspan: 3 }, { label: 'Q2 2025 (split mode)', colspan: 3, columnDropMode: 'split' }], ['January', 'February', 'March', 'April', 'May', 'June'] ]} ``` ::: ::: only-for angular ```ts nestedHeaders: [ // "Q1 2025" is cohesive (the default); "Q2 2025" splits when a column moves into it. [{ label: 'Q1 2025 (adopt mode)', colspan: 3 }, { label: 'Q2 2025 (split mode)', colspan: 3, columnDropMode: 'split' }], ['January', 'February', 'March', 'April', 'May', 'June'], ]; ``` ::: ::: only-for vue ```js nestedHeaders: [ // "Q1 2025" is cohesive (the default); "Q2 2025" splits when a column moves into it. [{ label: 'Q1 2025 (adopt mode)', colspan: 3 }, { label: 'Q2 2025 (split mode)', colspan: 3, columnDropMode: 'split' }], ['January', 'February', 'March', 'April', 'May', 'June'], ], ``` ::: In the example below, both quarters group three months. **Q1 2025** uses adopt mode (the default) and **Q2 2025** uses split mode. Drag a month from **Q2 2025** into the middle of **Q1 2025**: the **Q1 2025** group adopts it and spans four columns. Drag a month from **Q1 2025** into the middle of **Q2 2025**: the **Q2 2025** group splits into two **Q2 2025** banners around the inserted column. Drag a **Q2 2025** month out and then back into **Q2 2025**: it rejoins the single banner, because it belongs to that group. ::: only-for javascript ::: example #example4 --js 1 --ts 2 @[code](@/content/guides/columns/column-groups/javascript/example4.js) @[code](@/content/guides/columns/column-groups/javascript/example4.ts) ::: ::: ::: only-for react ::: example #example4 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-groups/react/example4.jsx) @[code](@/content/guides/columns/column-groups/react/example4.tsx) ::: ::: ::: only-for angular ::: example #example4 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-groups/angular/example4.ts) @[code](@/content/guides/columns/column-groups/angular/example4.html) ::: ::: ::: only-for vue ::: example #example4 :vue3 @[code](@/content/guides/columns/column-groups/vue/example4.vue) ::: ::: ## Known limitations - A column header can span up to 1000 columns, as the [HTML table specification](https://html.spec.whatwg.org/multipage/tables.html#dom-tdth-colspan) sets the limit of `colspan` to `1000`. - A nested column header can't be wider than its parent element (headers can't overlap). - If `rowspan` is larger than the number of header rows below the cell, Handsontable clamps it to the remaining header levels. ## Related keyboard shortcuts This header-focused shortcut works only when a collapsible column group header is focused. Enable [`navigableHeaders: true`](@/api/options.md#navigableheaders) to move focus onto headers with the arrow keys. For more details, see [Keyboard navigation](@/guides/accessibility/accessibility/accessibility.md#keyboard-navigation). | Windows | macOS | Action | Excel | Sheets | | ------------------------------------------- | -------------------------------------------- | ------------------------------------------------------- | :-----: | :-----: | | **Enter** | **Enter** | Collapse or expand the selected column group | ✗ | ✗ | ## Related API reference **Configuration options**
- [collapsibleColumns](@/api/options.md#collapsiblecolumns) - [nestedHeaders](@/api/options.md#nestedheaders) - [manualColumnMove](@/api/options.md#manualcolumnmove)
**Core methods**
- [isColumnModificationAllowed()](@/api/core.md#iscolumnmodificationallowed)
**Hooks**
- [afterColumnCollapse](@/api/hooks.md#aftercolumncollapse) - [afterColumnExpand](@/api/hooks.md#aftercolumnexpand) - [beforeColumnCollapse](@/api/hooks.md#beforecolumncollapse) - [beforeColumnExpand](@/api/hooks.md#beforecolumnexpand)
**Plugins**
- [CollapsibleColumns](@/api/collapsibleColumns.md) - [NestedHeaders](@/api/nestedHeaders.md)
--- # Column hiding URL: https://handsontable.com/docs/javascript-data-grid/column-hiding/ Hide individual columns to reduce screen clutter and improve the grid's performance. [[toc]] ## Overview "Hiding a column" means that the hidden column doesn't get rendered as a DOM element. When you're hiding a column: - The source data doesn't get modified. - The [`HiddenColumns`](@/api/hiddenColumns.md) plugin doesn't participate in data transformation
(the shape of the data returned by the [`getData*()` methods](@/api/core.md#getdata) stays intact). ## Enable column hiding To enable column hiding, use the [`hiddenColumns`](@/api/options.md#hiddencolumns) option. ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/columns/column-hiding/javascript/example1.js) @[code](@/content/guides/columns/column-hiding/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-hiding/react/example1.jsx) @[code](@/content/guides/columns/column-hiding/react/example1.tsx) ::: ::: [//]: # (The empty HTML comment above is a workaround for the `example` container error.) [//]: # (DON'T REMOVE IT!) ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-hiding/angular/example1.ts) @[code](@/content/guides/columns/column-hiding/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/columns/column-hiding/vue/example1.vue) ::: ::: ## Set up column hiding To set up your column hiding configuration, follow the steps below. ### Step 1: Specify columns hidden by default To both enable column hiding and specify columns hidden by default, set the [`hiddenColumns`](@/api/options.md#hiddencolumns) configuration option to an object. In the object, add a [`columns`](@/api/options.md#columns) configuration option, and set it to an array of column indexes. Now, those columns are hidden by default: ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/columns/column-hiding/javascript/example2.js) @[code](@/content/guides/columns/column-hiding/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-hiding/react/example2.jsx) @[code](@/content/guides/columns/column-hiding/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-hiding/angular/example2.ts) @[code](@/content/guides/columns/column-hiding/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/columns/column-hiding/vue/example2.vue) ::: ::: ### Step 2: Show UI indicators To easily see which columns are currently hidden, display UI indicators. To enable the UI indicators, in the [`hiddenColumns`](@/api/options.md#hiddencolumns) object, set the [`indicators`](@/api/hiddenColumns.md) property to `true`: ::: tip If you use both the [`NestedHeaders`](@/api/nestedHeaders.md) plugin and the [`HiddenColumns`](@/api/hiddenColumns.md) plugin, you also need to set the [`colHeaders`](@/api/options.md#colheaders) property to `true`. Otherwise, [`indicators`](@/api/hiddenColumns.md) won't work. ::: ::: only-for javascript ::: example #example3 --js 1 --ts 2 @[code](@/content/guides/columns/column-hiding/javascript/example3.js) @[code](@/content/guides/columns/column-hiding/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-hiding/react/example3.jsx) @[code](@/content/guides/columns/column-hiding/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-hiding/angular/example3.ts) @[code](@/content/guides/columns/column-hiding/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/columns/column-hiding/vue/example3.vue) ::: ::: ### Step 3: Set up context menu items To easily hide and unhide columns, add column hiding items to Handsontable's [context menu](@/guides/accessories-and-menus/context-menu/context-menu.md). Enable both the [`ContextMenu`](@/api/contextMenu.md) plugin and the [`HiddenColumns`](@/api/hiddenColumns.md) plugin. Now, the context menu automatically displays additional items for hiding and unhiding columns. ::: only-for javascript ::: example #example4 --js 1 --ts 2 @[code](@/content/guides/columns/column-hiding/javascript/example4.js) @[code](@/content/guides/columns/column-hiding/javascript/example4.ts) ::: ::: ::: only-for react ::: example #example4 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-hiding/react/example4.jsx) @[code](@/content/guides/columns/column-hiding/react/example4.tsx) ::: ::: ::: only-for angular ::: example #example4 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-hiding/angular/example4.ts) @[code](@/content/guides/columns/column-hiding/angular/example4.html) ::: ::: ::: only-for vue ::: example #example4 :vue3 @[code](@/content/guides/columns/column-hiding/vue/example4.vue) ::: ::: You can also add the column hiding menu items individually, by adding the [`hidden_columns_show`](@/guides/accessories-and-menus/context-menu/context-menu.md#context-menu-with-specific-options) and [`hidden_columns_hide`](@/guides/accessories-and-menus/context-menu/context-menu.md#context-menu-with-specific-options) strings to the[ `contextMenu`](@/api/contextMenu.md) parameter: ::: only-for javascript ::: example #example5 --js 1 --ts 2 @[code](@/content/guides/columns/column-hiding/javascript/example5.js) @[code](@/content/guides/columns/column-hiding/javascript/example5.ts) ::: ::: ::: only-for react ::: example #example5 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-hiding/react/example5.jsx) @[code](@/content/guides/columns/column-hiding/react/example5.tsx) ::: ::: ::: only-for angular ::: example #example5 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-hiding/angular/example5.ts) @[code](@/content/guides/columns/column-hiding/angular/example5.html) ::: ::: ::: only-for vue ::: example #example5 :vue3 @[code](@/content/guides/columns/column-hiding/vue/example5.vue) ::: ::: ### Step 4: Set up copy and paste behavior By default, hidden columns are included in copying and pasting. To exclude hidden columns from copying and pasting, in the [`hiddenColumns`](@/api/hiddenColumns.md) object, set the [`copyPasteEnabled`](@/api/hiddenColumns.md) property to `false`: ::: only-for javascript ::: example #example6 --js 1 --ts 2 @[code](@/content/guides/columns/column-hiding/javascript/example6.js) @[code](@/content/guides/columns/column-hiding/javascript/example6.ts) ::: ::: ::: only-for react ::: example #example6 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-hiding/react/example6.jsx) @[code](@/content/guides/columns/column-hiding/react/example6.tsx) ::: ::: ::: only-for angular ::: example #example6 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-hiding/angular/example6.ts) @[code](@/content/guides/columns/column-hiding/angular/example6.html) ::: ::: ::: only-for vue ::: example #example6 :vue3 @[code](@/content/guides/columns/column-hiding/vue/example6.vue) ::: ::: ## Result After completing this guide, you can hide columns from the grid without changing source data. You can configure default hidden columns, UI indicators, context menu items, and copy-paste behavior. ## Column hiding API methods For the most popular column hiding tasks, use the API methods below. ::: only-for react ::: tip To use the Handsontable API, you'll need access to the Handsontable instance. You can do that by utilizing a reference to the `HotTable` component, and reading its `hotInstance` property. For more information, see the [Instance methods](@/guides/getting-started/react-methods/react-methods.md) page. ::: ::: ::: only-for vue ::: tip To use the Handsontable API, you'll need access to the Handsontable instance. You can do that by utilizing a reference to the `HotTable` component, and reading its `hotInstance` property. For more information, see the [Referencing the Handsontable instance](@/guides/getting-started/vue3-hot-reference/vue3-hot-reference.md) page. ::: ::: To see your changes, re-render your Handsontable instance with the [`render()`](@/api/core.md#render) method. ### Access the [`HiddenColumns`](@/api/hiddenColumns.md) plugin instance To access the [`HiddenColumns`](@/api/hiddenColumns.md) plugin instance, use the [`getPlugin()`](@/api/core.md#getplugin) method: ```js const plugin = hot.getPlugin('hiddenColumns'); ``` ### Hide a single column To hide a single column, use the [`hideColumn()`](@/api/hiddenColumns.md#hidecolumn) method: ```js const plugin = hot.getPlugin('hiddenColumns'); plugin.hideColumn(4); // re-render your Handsontable instance hot.render(); ``` ### Hide multiple columns To hide multiple columns: - Either pass column indexes as arguments to the [`hideColumn()`](@/api/hiddenColumns.md#hidecolumn) method - Or pass an array of column indexes to the [`hideColumns()`](@/api/hiddenColumns.md#hidecolumn) method ```js const plugin = hot.getPlugin('hiddenColumns'); plugin.hideColumn(0, 4, 6); // or plugin.hideColumns([0, 4, 6]); // re-render your Handsontable instance hot.render(); ``` ### Unhide a single column To unhide a single column, use the [`showColumn()`](@/api/hiddenColumns.md#showcolumn) method: ```js const plugin = hot.getPlugin('hiddenColumns'); plugin.showColumn(4); // re-render your Handsontable instance hot.render(); ``` ### Unhide multiple columns To unhide multiple columns: - Either pass column indexes as arguments to the [`showColumn()`](@/api/hiddenColumns.md#showcolumn) method - Or pass an array of column indexes to the [`showColumns()`](@/api/hiddenColumns.md#showcolumns) method ```js const plugin = hot.getPlugin('hiddenColumns'); plugin.showColumn(0, 4, 6); // or plugin.showColumns([0, 4, 6]); // re-render your Handsontable instance hot.render(); ``` ## Related API reference **Configuration options**
- [hiddenColumns](@/api/options.md#hiddencolumns)
**Hooks**
- [afterHideColumns](@/api/hooks.md#afterhidecolumns) - [afterUnhideColumns](@/api/hooks.md#afterunhidecolumns) - [beforeHideColumns](@/api/hooks.md#beforehidecolumns) - [beforeUnhideColumns](@/api/hooks.md#beforeunhidecolumns)
**Plugins**
- [HiddenColumns](@/api/hiddenColumns.md)
--- # Adding and removing columns URL: https://handsontable.com/docs/javascript-data-grid/column-adding/ Insert and remove columns programmatically with the [`alter()`](@/api/core.md#alter) method, through the context menu, or automatically with spare columns. [[toc]] ## Insert and remove columns with the API Call the [`alter()`](@/api/core.md#alter) method to change the column structure programmatically. Pass an action name, a column index, and the number of columns to insert or remove: - `alter('insert_col_start', index, amount)` inserts columns to the left of the given column index. - `alter('insert_col_end', index, amount)` inserts columns to the right of the given column index. - `alter('remove_col', index, amount)` removes columns, starting at the given column index. In the example below, **Insert column** appends a column to the right of the last column, and **Remove last column** removes the last column. ::: only-for javascript ::: example #example1 --html 1 --js 2 --ts 3 @[code](@/content/guides/columns/column-adding/javascript/example1.html) @[code](@/content/guides/columns/column-adding/javascript/example1.js) @[code](@/content/guides/columns/column-adding/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-adding/react/example1.jsx) @[code](@/content/guides/columns/column-adding/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-adding/angular/example1.ts) @[code](@/content/guides/columns/column-adding/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/columns/column-adding/vue/example1.vue) ::: ::: The `index` argument uses visual column indexes. The `amount` argument defaults to `1` when omitted. ## Add and remove columns from the context menu Enable the [`contextMenu`](@/api/options.md#contextmenu) option to let users insert and remove columns by right-clicking a column. The relevant menu items are `col_left` (**Insert column left**), `col_right` (**Insert column right**), and `remove_col` (**Remove column**). Setting [`contextMenu`](@/api/options.md#contextmenu) to `true` shows the full default menu. To show only the column actions, pass an array of item keys, as in the example below. Right-click a column header or cell to open the menu. ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/columns/column-adding/javascript/example2.js) @[code](@/content/guides/columns/column-adding/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-adding/react/example2.jsx) @[code](@/content/guides/columns/column-adding/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-adding/angular/example2.ts) @[code](@/content/guides/columns/column-adding/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/columns/column-adding/vue/example2.vue) ::: ::: The [`allowInsertColumn`](@/api/options.md#allowinsertcolumn) and [`allowRemoveColumn`](@/api/options.md#allowremovecolumn) options control whether these context menu items are available. Both default to `true`. Set [`allowInsertColumn`](@/api/options.md#allowinsertcolumn) to `false` to hide the insert items, or [`allowRemoveColumn`](@/api/options.md#allowremovecolumn) to `false` to hide the remove item. ## Add spare columns automatically Set the [`minSpareCols`](@/api/options.md#minsparecols) option to keep a number of empty columns at the end of the grid. When a user enters data in the last empty column, Handsontable adds another empty column, so the grid always has at least the configured number of spare columns. ```js const configurationOptions = { // keep at least one empty column at the end of the grid minSpareCols: 1, }; ``` [`minSpareCols`](@/api/options.md#minsparecols) defaults to `0`. ## Control and react to column changes Use Handsontable's hooks to validate or respond to column changes: - [`beforeCreateCol`](@/api/hooks.md#beforecreatecol) runs before columns are inserted. Return `false` to cancel the insertion. - [`afterCreateCol`](@/api/hooks.md#aftercreatecol) runs after columns are inserted. - [`beforeRemoveCol`](@/api/hooks.md#beforeremovecol) runs before columns are removed. Return `false` to cancel the removal. - [`afterRemoveCol`](@/api/hooks.md#afterremovecol) runs after columns are removed. For example, block removing the last remaining column: ```js const configurationOptions = { beforeRemoveCol(index, amount) { if (this.countCols() - amount < 1) { // cancel the removal return false; } }, }; ``` ## Result After completing this guide, you can insert and remove columns with the [`alter()`](@/api/core.md#alter) method, let users add and remove columns through the context menu, keep spare columns at the end of the grid, and validate or react to column changes with hooks. ## Related API reference **Configuration options**
- [allowInsertColumn](@/api/options.md#allowinsertcolumn) - [allowRemoveColumn](@/api/options.md#allowremovecolumn) - [contextMenu](@/api/options.md#contextmenu) - [minSpareCols](@/api/options.md#minsparecols)
**Core methods**
- [alter()](@/api/core.md#alter) - [countCols()](@/api/core.md#countcols)
**Hooks**
- [afterCreateCol](@/api/hooks.md#aftercreatecol) - [afterRemoveCol](@/api/hooks.md#afterremovecol) - [beforeCreateCol](@/api/hooks.md#beforecreatecol) - [beforeRemoveCol](@/api/hooks.md#beforeremovecol)
--- # Column moving URL: https://handsontable.com/docs/javascript-data-grid/column-moving/ Change the order of columns, either manually (dragging them to another location), or programmatically (using Handsontable's API methods). [[toc]] ## Enable the plugin To enable column moving, set the [`manualColumnMove`](@/api/options.md#manualcolumnmove) configuration option to `true`. A draggable move handle appears above the selected column header. You can click and drag it to any location in the grid. A column has to be selected before you can drag it. You can start the drag anywhere on the selected column's header, including on the sorting label when [column sorting](@/guides/rows/rows-sorting/rows-sorting.md) is enabled. Handsontable tells a click from a drag by whether the pointer moves: press and release without moving to sort the column, and press and drag to move it. When column sorting is enabled, only the header label and its sort indicator sort on click. Pressing the header around them selects the column without sorting it, so you can select a column and drag it in one gesture. ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/columns/column-moving/javascript/example1.js) @[code](@/content/guides/columns/column-moving/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-moving/react/example1.jsx) @[code](@/content/guides/columns/column-moving/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-moving/angular/example1.ts) @[code](@/content/guides/columns/column-moving/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/columns/column-moving/vue/example1.vue) ::: ::: #### Move column headers When you move columns, the default column headers (A, B, C) stay in place. ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/columns/column-moving/javascript/example2.js) @[code](@/content/guides/columns/column-moving/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-moving/react/example2.jsx) @[code](@/content/guides/columns/column-moving/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-moving/angular/example2.ts) @[code](@/content/guides/columns/column-moving/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/columns/column-moving/vue/example2.vue) ::: ::: But, if you configure the [`colHeaders`](@/api/options.md#colheaders) option with your own column labels (e.g., One, Two, Three), your headers move along with the columns. ::: only-for javascript ::: example #example3 --js 1 --ts 2 @[code](@/content/guides/columns/column-moving/javascript/example3.js) @[code](@/content/guides/columns/column-moving/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-moving/react/example3.jsx) @[code](@/content/guides/columns/column-moving/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-moving/angular/example3.ts) @[code](@/content/guides/columns/column-moving/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/columns/column-moving/vue/example3.vue) ::: ::: ## Set a pre-defined column order Instead of setting [`manualColumnMove`](@/api/options.md#manualcolumnmove) to `true`, you can pass an **array of physical column indexes** to define the initial visual order of columns on render. Each position in the array corresponds to a visual (display) position, and the value at that position is the physical (source data) column index. For example: ```js manualColumnMove: [1, 0, 2] ``` This renders the columns in the following order: - Visual position 0 → physical column `1` - Visual position 1 → physical column `0` - Visual position 2 → physical column `2` The array must contain all physical column indexes (its length must equal the total number of columns). After the initial render, users can still drag columns to change the order further. For more on how physical and visual indexes relate, see [Understanding data and indexes](@/guides/getting-started/understanding-data-and-indexes/understanding-data-and-indexes.md). ## Data model behavior Moving columns does not reorder your source data. Handsontable stores the new order as index metadata through its [`IndexMapper`](@/api/indexMapper.md), and leaves the original data untouched. This affects how you read and save the data: - [`getData()`](@/api/core.md#getdata) returns cells in their current visual order, so it reflects any moves. Call it inside the [`afterColumnMove`](@/api/hooks.md#aftercolumnmove) hook to get an order-accurate snapshot to persist. - [`getSourceData()`](@/api/core.md#getsourcedata) returns cells in their original physical order, ignoring any moves. ### Don't feed the snapshot back into the grid Sending the reordered snapshot back to the grid as its new data applies the move a second time. [`updateData()`](@/api/core.md#updatedata) keeps the current column order on purpose, so Handsontable re-applies the order map it already holds on top of your already-reordered data. One drag then moves the column twice. Treat the snapshot as output only. Send it to your backend, and leave the grid's own data alone. ::: only-for react angular vue The data you bind to [`data`](@/api/options.md#data) does not change when a user moves a column, so you have to decide who owns the order. The two models are the same as for rows, and mixing them causes the same double move -- see [Choose who owns the row order](@/guides/rows/row-moving/row-moving.md#choose-who-owns-the-row-order). If you let Handsontable own the order, seed the starting order with [`initialState`](@/api/options.md#initialstate) rather than [`manualColumnMove`](@/api/options.md#manualcolumnmove). Handsontable reads [`initialState`](@/api/options.md#initialstate) only when it creates the grid, so a re-render can't apply the order a second time: ```js initialState: { manualColumnMove: [1, 0, 2], }, ``` The array both enables column moving and sets the starting order, so don't also pass [`manualColumnMove`](@/api/options.md#manualcolumnmove) at the top level. A regular setting takes precedence over the same key in [`initialState`](@/api/options.md#initialstate), so `manualColumnMove: true` alongside the code above would discard the order. If you own the order yourself, cancel the move by returning `false` from [`beforeColumnMove`](@/api/hooks.md#beforecolumnmove) and reorder your own data. Two things follow from that. [`afterColumnMove`](@/api/hooks.md#aftercolumnmove) never fires. The move stops at [`beforeColumnMove`](@/api/hooks.md#beforecolumnmove), before that hook runs, so persist the order from your own update rather than from the snapshot recipe above. Reordering columns also takes more work than reordering rows. You either reorder each row's cells, or reorder your [`columns`](@/api/options.md#columns) definitions. Reordering the cells is the safer route: passing `columns` through [`updateSettings()`](@/api/core.md#updatesettings) resets the states tied to rows and columns, including the row and column sequence, column widths, row heights, and frozen columns. ::: ## Control column moving Use the [`beforeColumnMove`](@/api/hooks.md#beforecolumnmove) hook to decide whether each column move is allowed. Returning `false` cancels the move while keeping the [`manualColumnMove`](@/api/options.md#manualcolumnmove) plugin enabled. Both [`beforeColumnMove`](@/api/hooks.md#beforecolumnmove) and [`afterColumnMove`](@/api/hooks.md#aftercolumnmove) run only when the pointer actually drags a column. A click on a column header does not fire them. In the following example, select **Allow column moving** before you drag a column to a new position. Clear the checkbox to block column moving again. :::: only-for javascript ::: example #example4 --html 1 --js 2 --ts 3 @[code](@/content/guides/columns/column-moving/javascript/example4.html) @[code](@/content/guides/columns/column-moving/javascript/example4.js) @[code](@/content/guides/columns/column-moving/javascript/example4.ts) ::: :::: :::: only-for react ::: example #example4 :react --jsx 1 --tsx 2 @[code](@/content/guides/columns/column-moving/react/example4.jsx) @[code](@/content/guides/columns/column-moving/react/example4.tsx) ::: :::: :::: only-for angular ::: example #example4 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-moving/angular/example4.ts) @[code](@/content/guides/columns/column-moving/angular/example4.html) ::: :::: :::: only-for vue ::: example #example4 :vue3 @[code](@/content/guides/columns/column-moving/vue/example4.vue) ::: :::: ## Result After completing this guide, you can reorder columns by dragging them with the mouse or by calling `dragColumns()` and `moveColumns()` programmatically. You can also set a pre-defined column order at initialization or use `beforeColumnMove` to block individual moves. ## Drag and move actions of the [`ManualColumnMove`](@/api/manualColumnMove.md) plugin There are significant differences between the plugin's [`dragColumns`](@/api/manualColumnMove.md#dragcolumns) and [`moveColumns`](@/api/manualColumnMove.md#movecolumns) API functions. Both of them change the order of columns, but they rely on different kinds of indexes. The differences between them are shown in the diagrams below. Both of these methods trigger the [`beforeColumnMove`](@/api/hooks.md#beforecolumnmove) and [`afterColumnMove`](@/api/hooks.md#aftercolumnmove) hooks, but only [`dragColumns`](@/api/manualColumnMove.md#dragcolumns) passes the [`dropIndex`](@/api/manualColumnMove.md#dragcolumns) argument to them. The [`dragColumns`](@/api/manualColumnMove.md#dragcolumns) method has a [`dropIndex`](@/api/manualColumnMove.md#dragcolumns) parameter, which points to where the elements are being dropped. ![dragColumns method](/img/drag_action.svg) The [`moveColumns`](@/api/manualColumnMove.md#movecolumns) method has a `finalIndex` parameter, which points to where the elements will be placed after the _moving_ action - `finalIndex` being the index of the first moved element. ![moveColumns method](/img/move_action.svg) The [`moveColumns`](@/api/manualColumnMove.md#movecolumns) function cannot perform some actions, e.g., more than one element can't be moved to the last position. In this scenario, the move will be cancelled. The Plugin's [`isMovePossible`](@/api/manualColumnMove.md#ismovepossible) API method and the `movePossible` parameters [`beforeColumnMove`](@/api/hooks.md#beforecolumnmove) and [`afterColumnMove`](@/api/hooks.md#aftercolumnmove) hooks help in determine such situations. ## Related API reference **Configuration options**
- [manualColumnMove](@/api/options.md#manualcolumnmove)
**Core methods**
- [colToProp()](@/api/core.md#coltoprop) - [isColumnModificationAllowed()](@/api/core.md#iscolumnmodificationallowed) - [propToCol()](@/api/core.md#proptocol) - [toPhysicalColumn()](@/api/core.md#tophysicalcolumn) - [toVisualColumn()](@/api/core.md#tovisualcolumn)
**Hooks**
- [afterColumnMove](@/api/hooks.md#aftercolumnmove) - [beforeColumnMove](@/api/hooks.md#beforecolumnmove)
**Plugins**
- [ManualColumnMove](@/api/manualColumnMove.md)
--- # Column freezing URL: https://handsontable.com/docs/javascript-data-grid/column-freezing/ Lock the position of specified columns, keeping them visible when scrolling. [[toc]] ## Overview Column freezing locks specific columns of a grid in place, keeping them visible while scrolling to another area of the grid. We refer to frozen columns as _fixed_. You can freeze columns during initialization and by the user. ## Freeze columns at initialization To freeze columns at initialization, use the [`fixedColumnsStart`](@/api/options.md#fixedcolumnsstart) option. Then, configure the container of your grid with the following CSS attributes: `width` and `overflow: hidden`. If your [layout direction](@/guides/internationalization/layout-direction/layout-direction.md) is `ltr`, columns get frozen from the left side of the table. If your layout direction is `rtl`, columns get frozen from the right side of the table. ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/columns/column-freezing/javascript/example1.js) @[code](@/content/guides/columns/column-freezing/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-freezing/react/example1.jsx) @[code](@/content/guides/columns/column-freezing/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-freezing/angular/example1.ts) @[code](@/content/guides/columns/column-freezing/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/columns/column-freezing/vue/example1.vue) ::: ::: ## User-triggered freeze To enable manual column freezing, set [`manualColumnFreeze`](@/api/options.md#manualcolumnfreeze) to `true`. This lets you freeze and unfreeze columns by using the grid's [context menu](@/guides/accessories-and-menus/context-menu/context-menu.md). Mind that when you unfreeze a frozen column, it doesn't go back to the original position. ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/columns/column-freezing/javascript/example2.js) @[code](@/content/guides/columns/column-freezing/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-freezing/react/example2.jsx) @[code](@/content/guides/columns/column-freezing/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-freezing/angular/example2.ts) @[code](@/content/guides/columns/column-freezing/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/columns/column-freezing/vue/example2.vue) ::: ::: ## Related API reference **Configuration options**
- [fixedColumnsStart](@/api/options.md#fixedcolumnsstart) - [manualColumnFreeze](@/api/options.md#manualcolumnfreeze)
**Plugins**
- [ManualColumnFreeze](@/api/manualColumnFreeze.md)
--- # Column widths URL: https://handsontable.com/docs/javascript-data-grid/column-width/ Configure column widths, using an array or a function. Let your users manually change column widths using Handsontable's interface. [[toc]] ## Overview By default, the column width adjusts to the width of the content. However, if the width of the content is less than `50px`, including `1px` for borders on the sides, the column width remains constant at `50px`. You can pass the column size as a constant, an array, or a function. The content inside a cell will be wrapped if it doesn't fit the cell's width. ## Set the column width as a constant In this example we set the same width of `100px` for all columns across the entire grid. ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/columns/column-width/javascript/example1.js) @[code](@/content/guides/columns/column-width/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-width/react/example1.jsx) @[code](@/content/guides/columns/column-width/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-width/angular/example1.ts) @[code](@/content/guides/columns/column-width/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/columns/column-width/vue/example1.vue) ::: ::: ## Set the column width in an array In this example, the width is only set for the first four columns. Each additional column would automatically adjust to the content. ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/columns/column-width/javascript/example2.js) @[code](@/content/guides/columns/column-width/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-width/react/example2.jsx) @[code](@/content/guides/columns/column-width/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-width/angular/example2.ts) @[code](@/content/guides/columns/column-width/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/columns/column-width/vue/example2.vue) ::: ::: ## Set the column width using a function In this example, the size of all columns is set using a function by taking a column `index` (1, 2 ...) and multiplying it by `40px` for each consecutive column. ::: only-for javascript ::: example #example3 --js 1 --ts 2 @[code](@/content/guides/columns/column-width/javascript/example3.js) @[code](@/content/guides/columns/column-width/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-width/react/example3.jsx) @[code](@/content/guides/columns/column-width/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-width/angular/example3.ts) @[code](@/content/guides/columns/column-width/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/columns/column-width/vue/example3.vue) ::: ::: ## Set a dynamic maximum column width Use the [`modifyColWidth`](@/api/hooks.md#modifycolwidth) hook to cap how wide a column can grow based on its content. The hook receives the width that Handsontable already calculated for the column. Return a smaller number to limit it. This pattern is [middleware](@/guides/getting-started/events-and-hooks/events-and-hooks.md#middleware): you modify a value on its way through the width pipeline instead of setting a fixed size up front. Unlike [`colWidths`](@/api/options.md#colwidths), a cap in `modifyColWidth` does not force every cell onto one width. Short values keep a narrow column on a single line. When content would exceed your threshold, the column shrinks to your cap and the text wraps. Leave `colWidths` unset for columns you want to auto-size (or set the entry to `undefined` in an array). The [`AutoColumnSize`](@/api/autoColumnSize.md) plugin must stay enabled so Handsontable can measure content first. Setting `colWidths` as a single number disables auto-sizing for all columns. You can also enforce a minimum width by returning `Math.max(width, minWidth)` from the same hook. ::: tip - Auto-sized columns never go below 50px. - [`stretchH`](@/api/options.md#stretchh) runs after your hook and can widen columns beyond your cap. - When [`manualColumnResize`](@/api/options.md#manualcolumnresize) is enabled, a width the user set by dragging overrides the hook for that column. ::: ::: only-for javascript ::: example #example7 --js 1 --ts 2 @[code](@/content/guides/columns/column-width/javascript/example7.js) @[code](@/content/guides/columns/column-width/javascript/example7.ts) ::: ::: ::: only-for react ::: example #example7 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-width/react/example7.jsx) @[code](@/content/guides/columns/column-width/react/example7.tsx) ::: ::: ::: only-for angular ::: example #example7 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-width/angular/example7.ts) @[code](@/content/guides/columns/column-width/angular/example7.html) ::: ::: ::: only-for vue ::: example #example7 :vue3 @[code](@/content/guides/columns/column-width/vue/example7.vue) ::: ::: ## Adjust the column width manually Set the option [`manualColumnResize`](@/api/options.md#manualcolumnresize) to `true` to allow users to manually resize the column width by dragging the handle between the adjacent column headers. If you double-click on that handle, the width will be instantly adjusted to the size of the longest value in the column. Don't forget to enable column headers by setting [`colHeaders`](@/api/options.md#colheaders) to `true`. You can adjust the size of one or multiple columns simultaneously, even if the selected columns are not placed next to each other. ::: tip When you set a column width programmatically with [`ManualColumnResize#setManualSize()`](@/api/manualColumnResize.md#setmanualsize), call `hot.render()` after the method call to repaint the grid. Values below `20px` are stored as `20px`. ::: ::: only-for javascript ::: example #example4 --js 1 --ts 2 @[code](@/content/guides/columns/column-width/javascript/example4.js) @[code](@/content/guides/columns/column-width/javascript/example4.ts) ::: ::: ::: only-for react ::: example #example4 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-width/react/example4.jsx) @[code](@/content/guides/columns/column-width/react/example4.tsx) ::: ::: ::: only-for angular ::: example #example4 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-width/angular/example4.ts) @[code](@/content/guides/columns/column-width/angular/example4.html) ::: ::: ::: only-for vue ::: example #example4 :vue3 @[code](@/content/guides/columns/column-width/vue/example4.vue) ::: ::: ## Column stretching You can adjust the width of the columns to make them fit the table's width automatically. The width of a particular column will be calculated based on the size and number of other columns in the grid. This option only makes sense when you have at least one column in your data set and fewer columns than needed to enable the horizontal scrollbar. ::: tip Use the [context menu](@/guides/accessories-and-menus/context-menu/context-menu.md) to insert or remove columns. This will help you understand how the grid reacts to changes. ::: ### Fit all columns equally This example fits all columns to the container's width equally by setting the option [`stretchH: 'all'`](@/api/options.md#stretchh). ::: only-for javascript ::: example #example5 --js 1 --ts 2 @[code](@/content/guides/columns/column-width/javascript/example5.js) @[code](@/content/guides/columns/column-width/javascript/example5.ts) ::: ::: ::: only-for react ::: example #example5 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-width/react/example5.jsx) @[code](@/content/guides/columns/column-width/react/example5.tsx) ::: ::: ::: only-for angular ::: example #example5 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-width/angular/example5.ts) @[code](@/content/guides/columns/column-width/angular/example5.html) ::: ::: ::: only-for vue ::: example #example5 :vue3 @[code](@/content/guides/columns/column-width/vue/example5.vue) ::: ::: ### Stretch only the last column In this example, the first three columns are set to be 80px wide, and the last column automatically fills the remaining space. This is achieved by setting the option [`stretchH: 'last'`](@/api/options.md#stretchh). ::: only-for javascript ::: example #example6 --js 1 --ts 2 @[code](@/content/guides/columns/column-width/javascript/example6.js) @[code](@/content/guides/columns/column-width/javascript/example6.ts) ::: ::: ::: only-for react ::: example #example6 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-width/react/example6.jsx) @[code](@/content/guides/columns/column-width/react/example6.tsx) ::: ::: ::: only-for angular ::: example #example6 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-width/angular/example6.ts) @[code](@/content/guides/columns/column-width/angular/example6.html) ::: ::: ::: only-for vue ::: example #example6 :vue3 @[code](@/content/guides/columns/column-width/vue/example6.vue) ::: ::: ### Column stretching and manual resizing Column stretching interacts with [`manualColumnResize`](@/api/options.md#manualcolumnresize) in two ways: - When `stretchH` is `'all'` or `'last'` together with `manualColumnResize: true`, you can still drag to resize a column. Stretching then recalculates, and the other columns adjust so the grid keeps filling the container. If your manual widths add up to more than the container width, stretching stops and the columns keep the sizes you set. - When you pass `manualColumnResize` an array of pre-defined widths (for example, `manualColumnResize: [80, 100, 120]`), the columns with explicit widths are excluded from stretching. Only the remaining columns are stretched to fill the container. ## A note about the performance As mentioned above, the default width of the column is based on the widest value in any cell within the column. You may be wondering how it's possible for data sets containing hundreds of thousands of records. This feature is made possible thanks to the [`AutoColumnSize`](@/api/autoColumnSize.md) plugin, which is enabled by default. Internally it divides the data set into smaller sets and renders only some of them to measure their size. The size is then applied to the entire column based on the width of the widest found value. To increase the performance, you can turn off this feature by defining the fixed size for the specified column or all columns. ::: tip If you call [`scrollViewportTo()`](@/api/core.md#scrollviewportto) and your columns have non-standard widths (for example, set by a custom renderer or CSS), make sure `AutoColumnSize` is enabled. Without it, the method may scroll to an incorrect position. ::: ## Size of the container Setting the dimensions of the container that holds Handsontable is described in detail on the [Grid size](@/guides/getting-started/grid-size/grid-size.md) page. ## Related API reference **Configuration options**
- [autoColumnSize](@/api/options.md#autocolumnsize) - [colWidths](@/api/options.md#colwidths) - [manualColumnResize](@/api/options.md#manualcolumnresize) - [stretchH](@/api/options.md#stretchh)
**Core methods**
- [getColWidth()](@/api/core.md#getcolwidth) - [scrollViewportTo()](@/api/core.md#scrollviewportto)
**Hooks**
- [afterColumnResize](@/api/hooks.md#aftercolumnresize) - [beforeColumnResize](@/api/hooks.md#beforecolumnresize) - [beforeStretchingColumnWidth](@/api/hooks.md#beforestretchingcolumnwidth) - [modifyAutoColumnSizeSeed](@/api/hooks.md#modifyautocolumnsizeseed) - [modifyColWidth](@/api/hooks.md#modifycolwidth)
**Plugins**
- [AutoColumnSize](@/api/autoColumnSize.md) - [ManualColumnResize](@/api/manualColumnResize.md)
--- # Column summary URL: https://handsontable.com/docs/javascript-data-grid/column-summary/ Calculate sum, min, max, count, average or custom aggregates of individual columns' data, using Handsontable's aggregate functions. [[toc]] ## Overview The [`ColumnSummary`](@/api/columnSummary.md) plugin lets you quickly calculate and display a column summary. Handsontable's aggregates are column-based: `ColumnSummary` summarizes a range of rows within a column. There is no separate row summary (`rowSummary`) feature. To show a per-row total across columns, compute it in your data source or a [custom renderer](@/guides/cell-functions/cell-renderer/cell-renderer.md). To customize your column summaries, you can: - Decide how a summary is calculated: - Either select one of the [built-in summary functions](#built-in-summary-functions) - Or implement a [custom summary function](#implement-a-custom-summary-function) - [Select columns and ranges of rows](#step-2-select-cells-that-you-want-to-summarize) that you want to summarize - [Display your summary result](#step-4-provide-the-destination-cell-s-coordinates) in a specific cell ### Column summary example This example calculates and displays five different column summaries: ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/columns/column-summary/javascript/example1.js) @[code](@/content/guides/columns/column-summary/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-summary/react/example1.jsx) @[code](@/content/guides/columns/column-summary/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-summary/angular/example1.ts) @[code](@/content/guides/columns/column-summary/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/columns/column-summary/vue/example1.vue) ::: ::: ### Built-in summary functions To decide how a column summary is calculated, you can use one of the following summary functions: | Function | Description | | --------- | ------------------------------------------------------------------------------------------------------ | | `sum` | Returns the sum of all values in a column. | | `min` | Returns the lowest value in a column. | | `max` | Returns the highest value in a column. | | `count` | Returns the number of all non-empty cells in a column. | | `average` | Returns the sum of all values in a column,
divided by the number of non-empty cells in that column. | | `custom` | Lets you implement a [custom summary function](#implement-a-custom-summary-function). | ### Column summary options You can customize each of your column summaries with configuration options. For the full list of available options, see the [API reference](@/api/columnSummary.md#options). ## Set up a column summary To set up a column summary, follow the steps below. ### Step 1: Enable the [`ColumnSummary`](@/api/columnSummary.md) plugin To enable the [`ColumnSummary`](@/api/columnSummary.md) plugin, set the [`columnSummary`](@/api/options.md#columnsummary) configuration option to an array of objects. Each object represents a single column summary. ::: only-for javascript ```js import Handsontable from 'handsontable/base'; import { registerAllModules } from 'handsontable/registry'; // Register all Handsontable's modules. registerAllModules(); const hot = new Handsontable(document.querySelector('#example'), { licenseKey: 'non-commercial-and-evaluation', data: [ [1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15] ], colHeaders: true, rowHeaders: true, // set the `columnSummary` configuration option to an array of // objects columnSummary: [ {}, {} ], }); ``` ::: ::: only-for react ```jsx import { HotTable } from '@handsontable/react-wrapper'; import { registerAllModules } from 'handsontable/registry'; // register Handsontable's modules registerAllModules(); const ExampleComponent = () => { return ( ); }; ``` ::: ::: only-for angular ```ts import { GridSettings, HotTableModule } from "@handsontable/angular-wrapper"; const data = [ [1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15], ]; const configurationOptions: GridSettings = { licenseKey: "non-commercial-and-evaluation", colHeaders: true, rowHeaders: true, // set the `columnSummary` configuration option to an array of objects columnSummary: [{}, {}], }; ``` ```html ``` ::: ::: only-for vue ```js import { HotTable } from '@handsontable/vue3'; import { registerAllModules } from 'handsontable/registry'; registerAllModules(); const hotSettings = { licenseKey: 'non-commercial-and-evaluation', data: [ [1, 2, 3, 4, 5], [6, 7, 8, 9, 10], [11, 12, 13, 14, 15] ], colHeaders: true, rowHeaders: true, // set the `columnSummary` configuration option to an array of // objects columnSummary: [ {}, {} ], }; ``` ```html ``` ::: You can also set the [`columnSummary`](@/api/options.md#columnsummary) option [to a function](#set-up-column-summaries-using-a-function). ### Step 2: Select cells that you want to summarize By default, a column summary takes all cells of the column in which it displays its result (see the [`destinationColumn`](@/api/columnSummary.md#options) option in [step 4](#step-4-provide-the-destination-cell-s-coordinates)). To summarize any other column, use the [`sourceColumn`](@/api/columnSummary.md#options) option: ::: only-for javascript ```js columnSummary: [ { // set this column summary to summarize the first column // (i.e. a column with physical index `0`) sourceColumn: 0, }, { // set this column summary to summarize the second column // (i.e. a column with physical index `1`) sourceColumn: 1, } ] ``` ::: ::: only-for react ```jsx columnSummary={[ { // set this column summary to summarize the first column // (i.e. a column with physical index `0`) sourceColumn: 0, }, { // set this column summary to summarize the second column // (i.e. a column with physical index `1`) sourceColumn: 1, } ]} ``` ::: ::: only-for angular ```js columnSummary: [ { // set this column summary to summarize the first column // (i.e. a column with physical index `0`) sourceColumn: 0, }, { // set this column summary to summarize the second column // (i.e. a column with physical index `1`) sourceColumn: 1, }, ]; ``` ::: ::: only-for vue ```js columnSummary: [ { // set this column summary to summarize the first column // (i.e. a column with physical index `0`) sourceColumn: 0, }, { // set this column summary to summarize the second column // (i.e. a column with physical index `1`) sourceColumn: 1, } ] ``` ::: You can also summarize individual ranges of rows (rather than a whole column). To do this, set the [`ranges`](@/api/columnSummary.md#options) option to an array of arrays, where each array represents a single row range. ::: only-for javascript ```js columnSummary: [ { sourceColumn: 0, // set this column summary to only summarize rows with physical // indexes 0-2, 4, and 6-8 ranges: [ [0, 2], [4], [6, 8] ], }, { sourceColumn: 0, // set this column summary to only summarize rows with physical // indexes 0-5 ranges: [ [0, 5] ], } ] ``` ::: ::: only-for react ```jsx columnSummary={[ { sourceColumn: 0, // set this column summary to only summarize rows with physical // indexes 0-2, 4, and 6-8 ranges: [ [0, 2], [4], [6, 8] ], }, { sourceColumn: 0, // set this column summary to only summarize rows with physical // indexes 0-5 ranges: [ [0, 5] ], } ]} ``` ::: ::: only-for angular ```js columnSummary: [ { sourceColumn: 0, // set this column summary to only summarize rows with physical // indexes 0-2, 4, and 6-8 ranges: [[0, 2], [4], [6, 8]], }, { sourceColumn: 0, // set this column summary to only summarize rows with physical // indexes 0-5 ranges: [[0, 5]], }, ]; ``` ::: ::: only-for vue ```js columnSummary: [ { sourceColumn: 0, // set this column summary to only summarize rows with physical // indexes 0-2, 4, and 6-8 ranges: [ [0, 2], [4], [6, 8] ], }, { sourceColumn: 0, // set this column summary to only summarize rows with physical // indexes 0-5 ranges: [ [0, 5] ], } ] ``` ::: ### Step 3: Calculate your summary Now, decide how you want to calculate your column summary. You can: - Either select one of the [built-in summary functions](#built-in-summary-functions) - Or implement a [custom summary function](#implement-a-custom-summary-function) ::: only-for javascript ```js columnSummary: [ { sourceColumn: 0, // set this column summary to return the sum all values in the // summarized column type: 'sum', }, { sourceColumn: 1, // set this column summary to return the lowest value in the // summarized column type: 'min', }, ]; ``` ::: ::: only-for react ```jsx columnSummary={[ { sourceColumn: 0, // set this column summary to return the sum all values in the // summarized column type: 'sum', }, { sourceColumn: 1, // set this column summary to return the lowest value in the // summarized column type: 'min', } ]} ``` ::: ::: only-for angular ```js columnSummary: [ { sourceColumn: 0, // set this column summary to return the sum all values in the // summarized column type: "sum", }, { sourceColumn: 1, // set this column summary to return the lowest value in the // summarized column type: "min", }, ]; ``` ::: ::: only-for vue ```js columnSummary: [ { sourceColumn: 0, // set this column summary to return the sum all values in the // summarized column type: 'sum', }, { sourceColumn: 1, // set this column summary to return the lowest value in the // summarized column type: 'min', } ] ``` ::: ### Step 4: Provide the destination cell's coordinates To display your column summary result in a cell, provide the destination cell's coordinates. Set the [`destinationRow`](@/api/columnSummary.md#options) and [`destinationColumn`](@/api/columnSummary.md#options) options to the physical coordinates of your required cell. ::: only-for javascript ```js columnSummary: [ { sourceColumn: 0, type: 'sum', // set this column summary to display its result in cell (4, 0) destinationRow: 4, destinationColumn: 0, }, { sourceColumn: 1, type: 'min', // set this column summary to display its result in cell (4, 1) destinationRow: 4, destinationColumn: 1 } ] ``` ::: ::: only-for react ```jsx columnSummary={[ { sourceColumn: 0, type: 'sum', // set this column summary to display its result in cell (4, 0) destinationRow: 4, destinationColumn: 0 }, { sourceColumn: 1, type: 'min', // set this column summary to display its result in cell (4, 1) destinationRow: 4, destinationColumn: 1 } ]} ``` ::: ::: only-for angular ```js columnSummary: [ { sourceColumn: 0, type: "sum", // set this column summary to display its result in cell (4, 0) destinationRow: 4, destinationColumn: 0, }, { sourceColumn: 1, type: "min", // set this column summary to display its result in cell (4, 1) destinationRow: 4, destinationColumn: 1, }, ]; ``` ::: ::: only-for vue ```js columnSummary: [ { sourceColumn: 0, type: 'sum', // set this column summary to display its result in cell (4, 0) destinationRow: 4, destinationColumn: 0 }, { sourceColumn: 1, type: 'min', // set this column summary to display its result in cell (4, 1) destinationRow: 4, destinationColumn: 1 } ] ``` ::: ::: tip Don't change the [`className`](@/api/options.md#classname) metadata of the summary row. If you need to style the summary row, use the class name assigned automatically by the [`ColumnSummary`](@/api/columnSummary.md) plugin: `columnSummaryResult`. ::: ### Step 5: Make room for the destination cell The [`ColumnSummary`](@/api/columnSummary.md) plugin doesn't automatically add new rows to display its summary results. So, if you always want to display your column summary result below your existing rows, you need to: 1. Add an empty row to the bottom of your grid (to avoid overwriting your existing rows). 2. Reverse row coordinates for your column summary (to always display your summary result at the bottom). ::: tip To reverse row coordinates for your column summary, set the [`reversedRowCoords`](@/api/columnSummary.md#options) option to `true`, and adjust the [`destinationRow`](@/api/columnSummary.md#options) coordinate. ::: ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/columns/column-summary/javascript/example2.js) @[code](@/content/guides/columns/column-summary/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-summary/react/example2.jsx) @[code](@/content/guides/columns/column-summary/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-summary/angular/example2.ts) @[code](@/content/guides/columns/column-summary/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/columns/column-summary/vue/example2.vue) ::: ::: ## Set up column summaries, using a function Instead of [setting up the column summary options manually](#set-up-a-column-summary), you can provide the whole column summary configuration as a function that returns a required array of objects. The example below sets up five different column summaries. To do this, it: - Defines a function named `generateData` which generates an array of arrays with dummy numeric data, and which lets you add an empty row at the bottom of the grid (to make room for displaying column summaries) - Sets Handsontable's [`columnSummary`](@/api/options.md#columnsummary) configuration option to a function that: - Iterates over visible columns - For each visible column, adds a column summary with a configuration - To display the column summaries in the empty row added by `generateData`, sets the [`reversedRowCoords`](@/api/columnSummary.md#options) option to `true`, and the [`destinationRow`](@/api/columnSummary.md#options) option to `0` ::: only-for javascript ::: example #example7 --js 1 --ts 2 @[code](@/content/guides/columns/column-summary/javascript/example7.js) @[code](@/content/guides/columns/column-summary/javascript/example7.ts) ::: ::: ::: only-for react ::: example #example7 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-summary/react/example7.jsx) @[code](@/content/guides/columns/column-summary/react/example7.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-summary/angular/example3.ts) @[code](@/content/guides/columns/column-summary/angular/example3.html) ::: ::: ::: only-for vue ::: example #example7 :vue3 @[code](@/content/guides/columns/column-summary/vue/example7.vue) ::: ::: Using a function to provide a column summary configuration lets you set up all sorts of more complex column summaries. For example, you can sum subtotals for nested groups: ::: only-for javascript ::: example #example8 --js 1 --ts 2 @[code](@/content/guides/columns/column-summary/javascript/example8.js) @[code](@/content/guides/columns/column-summary/javascript/example8.ts) ::: ::: ::: only-for react ::: example #example8 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-summary/react/example8.jsx) @[code](@/content/guides/columns/column-summary/react/example8.tsx) ::: ::: ::: only-for angular ::: example #example4 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-summary/angular/example4.ts) @[code](@/content/guides/columns/column-summary/angular/example4.html) ::: ::: ::: only-for vue ::: example #example8 :vue3 @[code](@/content/guides/columns/column-summary/vue/example8.vue) ::: ::: ## Implement a custom summary function Apart from using the [built-in summary functions](#built-in-summary-functions), you can also implement your own custom function that performs any summary calculation you want. To implement a custom summary function: 1. [Set up your column summary](#set-up-a-column-summary). 2. In your [column summary object](#step-1-enable-the-columnsummary-plugin), set the [`type`](@/api/options.md#type) option to `'custom'`: ::: only-for javascript ```js columnSummary: [{ sourceColumn: 1, // set the `type` option to `'custom'` type: 'custom', destinationRow: 0, destinationColumn: 5, reversedRowCoords: true }] ``` ::: ::: only-for react ```js columnSummary={[{ sourceColumn: 1, // set the `type` option to `'custom'` type: 'custom', destinationRow: 0, destinationColumn: 5, reversedRowCoords: true }]} ``` ::: ::: only-for angular ```js columnSummary: [ { sourceColumn: 1, // set the `type` option to `'custom'` type: "custom", destinationRow: 0, destinationColumn: 5, reversedRowCoords: true, }, ]; ``` ::: ::: only-for vue ```js columnSummary: [{ sourceColumn: 1, // set the `type` option to `'custom'` type: 'custom', destinationRow: 0, destinationColumn: 5, reversedRowCoords: true }] ``` ::: 3. In your column summary object, add your custom summary function: ::: only-for javascript ```js columnSummary: [{ type: 'custom', destinationRow: 0, destinationColumn: 5, reversedRowCoords: true, // add your custom summary function customFunction: function(endpoint) { // implement your function here } }] ``` ::: ::: only-for react ```js columnSummary={[{ type: 'custom', destinationRow: 0, destinationColumn: 5, reversedRowCoords: true, // add your custom summary function customFunction: function(endpoint) { // implement your function here } }]} ``` ::: ::: only-for angular ```js columnSummary: [ { type: "custom", destinationRow: 0, destinationColumn: 5, reversedRowCoords: true, // add your custom summary function customFunction: function (endpoint) { // implement your function here }, }, ]; ``` ::: ::: only-for vue ```js columnSummary: [{ type: 'custom', destinationRow: 0, destinationColumn: 5, reversedRowCoords: true, // add your custom summary function customFunction: function(endpoint) { // implement your function here } }] ``` ::: This example implements a function that counts the number of even values in a column: ::: only-for javascript ::: example #example9 --js 1 --ts 2 @[code](@/content/guides/columns/column-summary/javascript/example9.js) @[code](@/content/guides/columns/column-summary/javascript/example9.ts) ::: ::: ::: only-for react ::: example #example9 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-summary/react/example9.jsx) @[code](@/content/guides/columns/column-summary/react/example9.tsx) ::: ::: ::: only-for angular ::: example #example5 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-summary/angular/example5.ts) @[code](@/content/guides/columns/column-summary/angular/example5.html) ::: ::: ::: only-for vue ::: example #example9 :vue3 @[code](@/content/guides/columns/column-summary/vue/example9.vue) ::: ::: ## Round a column summary result You can round a column summary result to a specific number of digits after the decimal point. To enable this feature, set the [`roundFloat`](@/api/columnSummary.md) option to your preferred number of digits between 0 and 100. See the following example: ::: only-for javascript ::: example #example12 --js 1 --ts 2 @[code](@/content/guides/columns/column-summary/javascript/example12.js) @[code](@/content/guides/columns/column-summary/javascript/example12.ts) ::: ::: ::: only-for react ::: example #example12 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-summary/react/example12.jsx) @[code](@/content/guides/columns/column-summary/react/example12.tsx) ::: ::: ::: only-for angular ::: example #example6 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-summary/angular/example6.ts) @[code](@/content/guides/columns/column-summary/angular/example6.html) ::: ::: ::: only-for vue ::: example #example12 :vue3 @[code](@/content/guides/columns/column-summary/vue/example12.vue) ::: ::: The [`roundFloat`](@/api/columnSummary.md) option accepts the following values: | Value | Behavior | | ----------------- | ------------------------------------------------------- | | `false` (default) | Don't round the column summary result. | | `true` | Round the result to 0 digits after the decimal point. | | Integer 0-100 (n) | Round the result to n digits after the decimal point. | | Integer < 0 | Round the result to 0 digits after the decimal point. | | Integer > 100 | Round the result to 100 digits after the decimal point. | | `'auto'` | Automatically adjust the number of digits after the decimal point so the entire number fits into 8 digits. | If you enable [`roundFloat`](@/api/columnSummary.md), the data type returned by Handsontable's data-retrieving methods (like [`getDataAtCell()`](@/api/core.md#getdataatcell)) changes from `number` to `string`. ## Handle non-numeric values To summarize a column that contains non-numeric data, you can: - Either force your column summary to treat non-numeric values as numeric values - Or throw an error whenever a non-numeric value is passed to your column summary - Or make your column summary skip any non-numeric values ### Force numeric values You can force your column summary to treat non-numeric values as numeric values. ::: tip The [`forceNumeric`](@/api/columnSummary.md) option uses JavaScript's `parseFloat()` function. This means that e.g., `3c` is treated as `3`, but `c3` is still treated as `c3`. ::: To enable this feature, set the [`forceNumeric`](@/api/columnSummary.md) option to `true` (by default, [`forceNumeric`](@/api/columnSummary.md) is set to `false`). For example: ::: only-for javascript ::: example #example10 --js 1 --ts 2 @[code](@/content/guides/columns/column-summary/javascript/example10.js) @[code](@/content/guides/columns/column-summary/javascript/example10.ts) ::: ::: ::: only-for react ::: example #example10 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-summary/react/example10.jsx) @[code](@/content/guides/columns/column-summary/react/example10.tsx) ::: ::: ::: only-for angular ::: example #example7 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-summary/angular/example7.ts) @[code](@/content/guides/columns/column-summary/angular/example7.html) ::: ::: ::: only-for vue ::: example #example10 :vue3 @[code](@/content/guides/columns/column-summary/vue/example10.vue) ::: ::: ### Throw data type errors You can throw a data type error whenever a non-numeric value is passed to your column summary. To throw data type errors, set the [`suppressDataTypeErrors`](@/api/columnSummary.md) option to `false` (by default, [`suppressDataTypeErrors`](@/api/columnSummary.md) is set to `true`). For example: ::: only-for javascript ::: example #example11 --html 1 --js 2 --ts 3 @[code](@/content/guides/columns/column-summary/javascript/example11.html) @[code](@/content/guides/columns/column-summary/javascript/example11.js) @[code](@/content/guides/columns/column-summary/javascript/example11.ts) ::: ::: ::: only-for react ::: example #example11 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-summary/react/example11.jsx) @[code](@/content/guides/columns/column-summary/react/example11.tsx) ::: ::: ::: only-for angular ::: example #example8 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-summary/angular/example8.ts) @[code](@/content/guides/columns/column-summary/angular/example8.html) ::: ::: ::: only-for vue ::: example #example11 :vue3 @[code](@/content/guides/columns/column-summary/vue/example11.vue) ::: ::: ## Result After completing this guide, your grid calculates and displays column aggregates using built-in or custom summary functions. You can target specific cell ranges, control rounding, and handle non-numeric values. ## Related API reference **Configuration options**
- [columnSummary](@/api/options.md#columnsummary)
**Plugins**
- [ColumnSummary](@/api/columnSummary.md)
--- # Column virtualization URL: https://handsontable.com/docs/javascript-data-grid/column-virtualization/ Render hundreds of columns without freezing the browser, using column virtualization. [[toc]] ## Overview To process a large number of columns in a browser Handsontable utilizes the virtualization process to display only the visible part of the grid with a small offset for a better scrolling experience. This feature is enabled by default and can be turned off by setting the [`renderAllColumns`](@/api/options.md#renderallcolumns) option to `true`. ## Configure the column virtualization You can experiment with the [`viewportColumnRenderingOffset`](@/api/options.md#viewportcolumnrenderingoffset) config option, which determines the number of columns being displayed outside the visible viewport. If the number passed to that option is greater than the total columns in your data set, the virtualization will be practically turned off. To make the grid scrollable, set the constant width and height to same as the container holding Handsontable and height and set the `overflow` property to `hidden` in the container's stylesheet. If the table contains enough rows or columns, it will be scrollable. The scrolling performance depends mainly on four factors: - Number of cells - number of rows multiplied by the number of columns - Amount and complexity of custom renderers in cells - Number of options enabled in the configuration - Performance of your setup - physical machine and browser The demo below presents a data grid displaying one million cells (1000 rows x 1000 columns). ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/columns/column-virtualization/javascript/example1.js) @[code](@/content/guides/columns/column-virtualization/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-virtualization/react/example1.jsx) @[code](@/content/guides/columns/column-virtualization/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-virtualization/angular/example1.ts) @[code](@/content/guides/columns/column-virtualization/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/columns/column-virtualization/vue/example1.vue) ::: ::: ## Horizontal scrolling Hold Shift and scroll the mouse wheel to scroll the grid horizontally. No configuration is required. ## Known limitations Using column virtualization has the following side effects: - The browser's native search will work only for the visible part of the grid. - Screen readers may announce the wrong total number of columns. Read more in the [Accessibility](@/guides/accessibility/accessibility/accessibility.md#disabling-dom-virtualization-for-improved-accessibility) guide. ## Related articles **Related guides**
- [Row virtualization](@/guides/rows/row-virtualization/row-virtualization.md) - [Performance](@/guides/optimization/performance/performance.md)
**Configuration options**
- [viewportColumnRenderingOffset](@/api/options.md#viewportcolumnrenderingoffset) - [renderAllColumns](@/api/options.md#renderallcolumns)
--- # Column filter URL: https://handsontable.com/docs/javascript-data-grid/column-filter/ Filter data by values or by a set of conditions, using Handsontable's intuitive user interface or flexible API. [[toc]] ## Overview Filtering lets you quickly find the information that you're looking for, based on specific criteria. This makes data analysis easier and faster, especially with large data sets. Handsontable's built-in filtering interface resembles Excel's, so it's intuitive even to non-technical users. And if you want to implement your own interface, you can easily filter data programmatically, using Handsontable's API. You can filter data by value, or use the built-in conditions, which are different for each of the available column types. ## Filtering demo Click on one of the column menu buttons (▼) and play around with filtering by selecting values or conditions-based criteria. After filtering, the column readjusts its width to the longest value displayed on screen. To disable this behavior, set [fixed column widths](@/guides/columns/column-width/column-width.md#set-the-column-width-as-a-constant). ::: only-for javascript ::: example #exampleFilterBasicDemo --html 1 --js 2 --ts 3 @[code](@/content/guides/columns/column-filter/javascript/exampleFilterBasicDemo.html) @[code](@/content/guides/columns/column-filter/javascript/exampleFilterBasicDemo.js) @[code](@/content/guides/columns/column-filter/javascript/exampleFilterBasicDemo.ts) ::: ::: ::: only-for react ::: example #exampleFilterBasicDemo :react --js 1 --ts 2 @[code](@/content/guides/columns/column-filter/react/exampleFilterBasicDemo.jsx) @[code](@/content/guides/columns/column-filter/react/exampleFilterBasicDemo.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-filter/angular/example1.ts) @[code](@/content/guides/columns/column-filter/angular/example1.html) ::: ::: ::: only-for vue ::: example #exampleFilterBasicDemo :vue3 @[code](@/content/guides/columns/column-filter/vue/exampleFilterBasicDemo.vue) ::: ::: ## Enable filtering To enable the filtering interface for all columns, you need to do two things: 1. Set the `filters` option to `true`. 2. Enable the interface by setting the `dropdownMenu` option to `true`. Enabling the `filters` option without the interface can be useful if you plan to create your own custom interface for filtering by using the API. ::: only-for javascript ```js const configurationOptions = { // enable filtering filters: true, // enable the column menu dropdownMenu: true, }; ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts const configurationOptions = { // enable filtering filters: true, // enable the column menu dropdownMenu: true, }; ``` ```html ``` ::: ::: only-for vue ```js const hotSettings = { // enable filtering filters: true, // enable the column menu dropdownMenu: true, }; ``` ```html ``` ::: By default, the column menu presents the filtering interface along with other default items such as **Insert column left**. To display only the filtering interface, pass an array of filter items in the configuration. ::: only-for javascript ::: example #exampleShowFilterItemsOnly --html 1 --js 2 --ts 3 @[code](@/content/guides/columns/column-filter/javascript/exampleShowFilterItemsOnly.html) @[code](@/content/guides/columns/column-filter/javascript/exampleShowFilterItemsOnly.js) @[code](@/content/guides/columns/column-filter/javascript/exampleShowFilterItemsOnly.ts) ::: ::: ::: only-for react ::: example #exampleShowFilterItemsOnly :react --js 1 --ts 2 @[code](@/content/guides/columns/column-filter/react/exampleShowFilterItemsOnly.jsx) @[code](@/content/guides/columns/column-filter/react/exampleShowFilterItemsOnly.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-filter/angular/example2.ts) @[code](@/content/guides/columns/column-filter/angular/example2.html) ::: ::: ::: only-for vue ::: example #exampleShowFilterItemsOnly :vue3 @[code](@/content/guides/columns/column-filter/vue/exampleShowFilterItemsOnly.vue) ::: ::: ### Enable filtering for individual columns You have control over which columns are filterable and for which columns the column menu is enabled. In the following demo, only the **Brand** column is filterable, while the other columns are not. However, the **Model** column still has the column menu available in case you want to have some useful items in the menu such as **Clear column**. ::: only-for javascript ::: example #exampleEnableFilterInColumns --html 1 --js 2 --ts 3 @[code](@/content/guides/columns/column-filter/javascript/exampleEnableFilterInColumns.html) @[code](@/content/guides/columns/column-filter/javascript/exampleEnableFilterInColumns.js) @[code](@/content/guides/columns/column-filter/javascript/exampleEnableFilterInColumns.ts) ::: ::: ::: only-for react ::: example #exampleEnableFilterInColumns :react --js 1 --ts 2 @[code](@/content/guides/columns/column-filter/react/exampleEnableFilterInColumns.jsx) @[code](@/content/guides/columns/column-filter/react/exampleEnableFilterInColumns.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-filter/angular/example3.ts) @[code](@/content/guides/columns/column-filter/angular/example3.html) ::: ::: ::: only-for vue ::: example #exampleEnableFilterInColumns :vue3 @[code](@/content/guides/columns/column-filter/vue/exampleEnableFilterInColumns.vue) ::: ::: ### Enable filtering within already filtered results To apply filters based on the search input, set `searchMode` to `'apply'`. You can then apply the filter by either pressing the **Enter** key while the search input is focused or by clicking the `OK` button. ::: only-for javascript ```js const configurationOptions = { // enable filtering filters: { searchMode: 'apply' }, // enable the column menu dropdownMenu: true, }; ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts const configurationOptions = { // enable filtering filters: { searchMode: 'apply' }, // enable the column menu dropdownMenu: true, }; ``` ```html ``` ::: ::: only-for vue ```js const hotSettings = { // enable filtering filters: { searchMode: 'apply' }, // enable the column menu dropdownMenu: true, }; ``` ```html ``` ::: ::: only-for javascript ::: example #exampleSearchMode --html 1 --js 2 --ts 3 @[code](@/content/guides/columns/column-filter/javascript/exampleSearchMode.html) @[code](@/content/guides/columns/column-filter/javascript/exampleSearchMode.js) @[code](@/content/guides/columns/column-filter/javascript/exampleSearchMode.ts) ::: ::: ::: only-for react ::: example #exampleSearchMode :react --js 1 --ts 2 @[code](@/content/guides/columns/column-filter/react/exampleSearchMode.jsx) @[code](@/content/guides/columns/column-filter/react/exampleSearchMode.tsx) ::: ::: ::: only-for angular ::: example #example12 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-filter/angular/example12.ts) @[code](@/content/guides/columns/column-filter/angular/example12.html) ::: ::: ::: only-for vue ::: example #exampleSearchMode :vue3 @[code](@/content/guides/columns/column-filter/vue/exampleSearchMode.vue) ::: ::: ## Filter different types of data With its built-in cell types, Handsontable makes it easy to handle common data types like text, numbers, and dates by providing numerous configuration options. In addition, the filtering feature is designed to understand the underlying data and provides an adaptive interface that is tailored to each data type. ::: only-for javascript ::: example #exampleFilterDifferentTypes --html 1 --js 2 --ts 3 @[code](@/content/guides/columns/column-filter/javascript/exampleFilterDifferentTypes.html) @[code](@/content/guides/columns/column-filter/javascript/exampleFilterDifferentTypes.js) @[code](@/content/guides/columns/column-filter/javascript/exampleFilterDifferentTypes.ts) ::: ::: ::: only-for react ::: example #exampleFilterDifferentTypes :react --js 1 --ts 2 @[code](@/content/guides/columns/column-filter/react/exampleFilterDifferentTypes.jsx) @[code](@/content/guides/columns/column-filter/react/exampleFilterDifferentTypes.tsx) ::: ::: ::: only-for angular ::: example #example4 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-filter/angular/example4.ts) @[code](@/content/guides/columns/column-filter/angular/example4.html) ::: ::: ::: only-for vue ::: example #exampleFilterDifferentTypes :vue3 @[code](@/content/guides/columns/column-filter/vue/exampleFilterDifferentTypes.vue) ::: ::: The following table contains all available filter operators for each built-in data type. | Cell type | Available filter operators | | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | All cell types | Default operators:

None
Is empty
Is not empty
Is equal to
Is not equal to | | text
time
checkbox
dropdown
autocomplete
password | Default operators plus:

Begins with
Ends with
Contains
Does not contain | | numeric | Default operators plus:

Greater than
Greater than or equal to
Less than
Less than or equal to
Is between
Is not between | | date | Default operators plus:

Before (exclusive -- boundary date excluded)
Before or equal to (boundary date included)
After (exclusive -- boundary date excluded)
After or equal to (boundary date included)
Is between
Tomorrow
Today
Yesterday | | intl-date | Default operators plus:

Before (exclusive -- boundary date excluded)
Before or equal to (boundary date included)
After (exclusive -- boundary date excluded)
After or equal to (boundary date included)
Is between
Tomorrow
Today
Yesterday | | intl-time | Default operators plus:

Begins with
Ends with
Contains
Does not contain
Before (exclusive -- boundary time excluded)
Before or equal to (boundary time included)
After (exclusive -- boundary time excluded)
After or equal to (boundary time included)
Is between | The **None** operator clears the column's filter. Its programmatic equivalent is [`filters.removeConditions(column)`](@/api/filters.md#removeconditions). For more on clearing filters with the API, see [Clear a column filter](#clear-a-column-filter). For the **Before**, **After**, **Before or equal to**, **After or equal to**, and **Is between** operators on `date`, `intl-date`, and `intl-time` columns, the filter menu shows a native date or time input. Pick the value from the browser's picker or type it in your locale's format. When you set the condition through the API instead, pass the value as an ISO 8601 string (`YYYY-MM-DD` for dates, `HH:mm` for times). ## Filter data on initialization You can filter data on Handsontable's initialization. This lets you apply pre-defined filters every time you launch your grid. ::: only-for javascript To do this, you can use Handsontable's [`afterInit()`](@/api/hooks.md#afterinit) hook, along with the API provided by the Filters plugin. For instance, the demo below demonstrates how you can start with a pre-applied filter to display only items priced less than $200. ::: example #exampleFilterOnInitialization --html 1 --js 2 --ts 3 @[code](@/content/guides/columns/column-filter/javascript/exampleFilterOnInitialization.html) @[code](@/content/guides/columns/column-filter/javascript/exampleFilterOnInitialization.js) @[code](@/content/guides/columns/column-filter/javascript/exampleFilterOnInitialization.ts) ::: ::: ::: only-for react To do this, use the API provided by the [`Filters`](@/api/filters.md) plugin. For instance, the demo below demonstrates how you can start with a pre-applied filter to display only items priced less than $200. ::: example #exampleFilterOnInitialization :react --js 1 --ts 2 @[code](@/content/guides/columns/column-filter/react/exampleFilterOnInitialization.jsx) @[code](@/content/guides/columns/column-filter/react/exampleFilterOnInitialization.tsx) ::: ::: ::: only-for angular To do this, you can use Handsontable's [`afterInit()`](@/api/hooks.md#afterinit) hook, along with the API provided by the Filters plugin. For instance, the demo below demonstrates how you can start with a pre-applied filter to display only items priced less than $200. ::: example #example5 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-filter/angular/example5.ts) @[code](@/content/guides/columns/column-filter/angular/example5.html) ::: ::: ::: only-for vue To do this, use the API provided by the [`Filters`](@/api/filters.md) plugin. For instance, the demo below demonstrates how you can start with a pre-applied filter to display only items priced less than $200. ::: example #exampleFilterOnInitialization :vue3 @[code](@/content/guides/columns/column-filter/vue/exampleFilterOnInitialization.vue) ::: ::: ## External quick filter Handsontable's quick filter feature lets you search for a particular phrase in a specific column. To accomplish this, use methods [`filters.addCondition()`](@/api/filters.md#addcondition) and [`filters.filter()`](@/api/filters.md#filter). ::: only-for javascript ::: example #exampleQuickFilter --html 1 --js 2 --ts 3 --css 4 @[code](@/content/guides/columns/column-filter/javascript/exampleQuickFilter.html) @[code](@/content/guides/columns/column-filter/javascript/exampleQuickFilter.js) @[code](@/content/guides/columns/column-filter/javascript/exampleQuickFilter.ts) @[code](@/content/guides/columns/column-filter/javascript/exampleQuickFilter.css) ::: ::: ::: only-for react ::: example #exampleQuickFilter :react --js 1 --css 2 --ts 3 @[code](@/content/guides/columns/column-filter/react/exampleQuickFilter.jsx) @[code](@/content/guides/columns/column-filter/react/exampleQuickFilter.css) @[code](@/content/guides/columns/column-filter/react/exampleQuickFilter.tsx) ::: ::: ::: only-for angular ::: example #example6 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-filter/angular/example6.ts) @[code](@/content/guides/columns/column-filter/angular/example6.html) ::: ::: ::: only-for vue ::: example #exampleQuickFilter :vue3 @[code](@/content/guides/columns/column-filter/vue/exampleQuickFilter.vue) ::: ::: ## Customize the filter button The default button that opens the column menu can be styled with CSS by modifying `button.changeType` variables and its `::before` pseudoclass that contains svg mask-image displaying an arrow down icon. ::: only-for javascript ::: example #exampleCustomFilterButton --html 1 --js 2 --ts 3 --css 4 @[code](@/content/guides/columns/column-filter/javascript/exampleCustomFilterButton.html) @[code](@/content/guides/columns/column-filter/javascript/exampleCustomFilterButton.js) @[code](@/content/guides/columns/column-filter/javascript/exampleCustomFilterButton.ts) @[code](@/content/guides/columns/column-filter/javascript/exampleCustomFilterButton.css) ::: ::: ::: only-for react ::: example #exampleCustomFilterButton :react --js 1 --css 2 --ts 3 @[code](@/content/guides/columns/column-filter/react/exampleCustomFilterButton.jsx) @[code](@/content/guides/columns/column-filter/react/exampleCustomFilterButton.css) @[code](@/content/guides/columns/column-filter/react/exampleCustomFilterButton.tsx) ::: ::: ::: only-for angular ::: example #example7 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-filter/angular/example7.ts) @[code](@/content/guides/columns/column-filter/angular/example7.html) ::: ::: ::: only-for vue ::: example #exampleCustomFilterButton :vue3 @[code](@/content/guides/columns/column-filter/vue/exampleCustomFilterButton.vue) ::: ::: The column menu button is always visible, but if you want it to appear only when the mouse cursor is over the header, apply additional styling to `th .relative:hover .changeType`. ::: only-for javascript ::: example #exampleCustomFilterButton2 --html 1 --js 2 --ts 3 --css 4 @[code](@/content/guides/columns/column-filter/javascript/exampleCustomFilterButton2.html) @[code](@/content/guides/columns/column-filter/javascript/exampleCustomFilterButton2.js) @[code](@/content/guides/columns/column-filter/javascript/exampleCustomFilterButton2.ts) @[code](@/content/guides/columns/column-filter/javascript/exampleCustomFilterButton2.css) ::: ::: ::: only-for react ::: example #exampleCustomFilterButton2 :react --js 1 --css 2 --ts 3 @[code](@/content/guides/columns/column-filter/react/exampleCustomFilterButton2.jsx) @[code](@/content/guides/columns/column-filter/react/exampleCustomFilterButton2.css) @[code](@/content/guides/columns/column-filter/react/exampleCustomFilterButton2.tsx) ::: ::: ::: only-for angular ::: example #example8 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-filter/angular/example8.ts) @[code](@/content/guides/columns/column-filter/angular/example8.html) ::: ::: ::: only-for vue ::: example #exampleCustomFilterButton2 :vue3 @[code](@/content/guides/columns/column-filter/vue/exampleCustomFilterButton2.vue) ::: ::: ## Change the width of the filter menu If the text data in your columns is too long to fit in the filters container, you can adjust the column menu's width for better user experience. You can achieve this with by styling `.htDropdownMenu table.htCore`. ```css .handsontable .htDropdownMenu table.htCore { width: 300px !important; } ``` ## Exclude rows from filtering You can exclude any number of top or bottom rows from filtering. In the following demo, the first and the last row are [frozen](@/guides/rows/row-freezing/row-freezing.md), and filtering doesn't affect them. ::: only-for javascript ::: example #exampleExcludeRowsFromFiltering --html 1 --js 2 --ts 3 @[code](@/content/guides/columns/column-filter/javascript/exampleExcludeRowsFromFiltering.html) @[code](@/content/guides/columns/column-filter/javascript/exampleExcludeRowsFromFiltering.js) @[code](@/content/guides/columns/column-filter/javascript/exampleExcludeRowsFromFiltering.ts) ::: ::: ::: only-for react ::: example #exampleExcludeRowsFromFiltering :react --js 1 --ts 2 @[code](@/content/guides/columns/column-filter/react/exampleExcludeRowsFromFiltering.jsx) @[code](@/content/guides/columns/column-filter/react/exampleExcludeRowsFromFiltering.tsx) ::: ::: ::: only-for angular ::: example #example9 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-filter/angular/example9.ts) @[code](@/content/guides/columns/column-filter/angular/example9.html) ::: ::: ::: only-for vue ::: example #exampleExcludeRowsFromFiltering :vue3 @[code](@/content/guides/columns/column-filter/vue/exampleExcludeRowsFromFiltering.vue) ::: ::: If you use [`bindRowsWithHeaders`](@/guides/rows/row-header/row-header.md#bind-rows-with-headers) to keep row headers tied to a row's position in the source data, filtering doesn't affect that binding. Filtering removes non-matching rows from view instead of renumbering the rows that remain, so bound row headers keep pointing to the same row after you filter or clear a filter. ## Server-side filtering You can decide to use Handsontable as an intuitive filtering interface, but perform the actual filtering on the server. To help you with that, Handsontable's [`beforeFilter()`](@/api/hooks.md#beforefilter) hook allows you to: - Gather information about the filters that the user wants to apply, to send it to the server. - Disable filtering on the front end, so it doesn't interfere with filtering on the server. In the following demo, click on one of the column menu buttons (▼) and play around with filtering by selecting values or conditions-based criteria. After you click **OK**, the ▼ button turns green to indicate filtering, but the data is not filtered. Instead, the information about the specified filters is logged to the console. ::: only-for javascript ::: example #exampleServerSideFilter --html 1 --js 2 --ts 3 @[code](@/content/guides/columns/column-filter/javascript/exampleServerSideFilter.html) @[code](@/content/guides/columns/column-filter/javascript/exampleServerSideFilter.js) @[code](@/content/guides/columns/column-filter/javascript/exampleServerSideFilter.ts) ::: ::: ::: only-for react ::: example #exampleServerSideFilter :react --js 1 --ts 2 @[code](@/content/guides/columns/column-filter/react/exampleServerSideFilter.jsx) @[code](@/content/guides/columns/column-filter/react/exampleServerSideFilter.tsx) ::: ::: ::: only-for angular ::: example #example10 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-filter/angular/example10.ts) @[code](@/content/guides/columns/column-filter/angular/example10.html) ::: ::: ::: only-for vue ::: example #exampleServerSideFilter :vue3 @[code](@/content/guides/columns/column-filter/vue/exampleServerSideFilter.vue) ::: ::: ## Control filtering programmatically You can control filtering at the grid's runtime by using Handsontable's [hooks](@/guides/getting-started/events-and-hooks/events-and-hooks.md) and [API methods](@/api/filters.md#methods). This allows you to enable or disable filtering based on specific conditions. For example, you may create a user interface outside of the grid to manage Handsontable's filtering behavior. ### Enable or disable filtering programmatically To enable or disable filtering programmatically, use the [`updateSettings()`](@/api/core.md#updatesettings) method. ::: only-for javascript ```js handsontableInstance.updateSettings({ // enable filtering filters: true, // enable the column menu dropdownMenu: true, }); handsontableInstance.updateSettings({ // disable filtering filters: false, }); ``` ::: ::: only-for react ```jsx const hotTableComponentRef = useRef(null); hotTableComponentRef.current.hotInstance.updateSettings({ // enable filtering filters: true, // enable the column menu dropdownMenu: true, }); hotTableComponentRef.current.hotInstance.updateSettings({ // disable filtering filters: false, }); ``` ::: ::: only-for angular ```ts @ViewChild(HotTableComponent, {static: false}) hotTable!: HotTableComponent; this.hotTable.hotInstance!.updateSettings({ // enable filtering filters: true, // enable the column menu dropdownMenu: true, }); this.hotTable.hotInstance!.updateSettings({ // disable filtering filters: false, }); ``` ::: ::: only-for vue ```js const hotTableRef = useTemplateRef>('hotTableRef'); hotTableRef.value.hotInstance.updateSettings({ // enable filtering filters: true, // enable the column menu dropdownMenu: true, }); hotTableRef.value.hotInstance.updateSettings({ // disable filtering filters: false, }); ``` ::: You can also enable or disable filtering for specific columns. For example, to enable filtering only for the first column: ::: only-for javascript ```js handsontableInstance.updateSettings({ // enable filtering, for all columns filters: true, // enable the column menu, for all columns // but display only the 'Filter by value' list and the 'OK' and // 'Cancel' buttons dropdownMenu: { items: { filter_by_value: { // hide the 'Filter by value' list from all columns but the // first one hidden() { return this.getSelectedRangeLast().to.col > 0; }, }, filter_action_bar: { // hide the 'OK' and 'Cancel' buttons from all columns but the // first one hidden() { return this.getSelectedRangeLast().to.col > 0; }, }, }, }, }); ``` ::: ::: only-for react ```jsx const hotTableComponentRef = useRef(null); hotTableComponentRef.current.hotInstance.updateSettings({ // enable filtering for all columns filters: true, // enable the column menu for all columns // but display only the 'Filter by value' list and the 'OK' and // 'Cancel' buttons dropdownMenu: { items: { filter_by_value: { // hide the 'Filter by value' list from all columns but the // first one hidden() { return this.getSelectedRangeLast().to.col > 0; }, }, filter_action_bar: { // hide the 'OK' and 'Cancel' buttons from all columns but the // first one hidden() { return this.getSelectedRangeLast().to.col > 0; }, }, }, }, }); ``` ::: ::: only-for angular ```ts @ViewChild(HotTableComponent, {static: false}) hotTable!: HotTableComponent; this.hotTable.hotInstance!.updateSettings({ // enable filtering, for all columns filters: true, // enable the column menu, for all columns // but display only the 'Filter by value' list and the 'OK' and 'Cancel' buttons dropdownMenu: { items: { filter_by_value: { // hide the 'Filter by value' list from all columns but the first one hidden() { return this.getSelectedRangeLast().to.col > 0; }, }, filter_action_bar: { // hide the 'OK' and 'Cancel' buttons from all columns but the first one hidden() { return this.getSelectedRangeLast().to.col > 0; }, }, }, }, }); ``` ::: ::: only-for vue ```js const hotTableRef = useTemplateRef>('hotTableRef'); hotTableRef.value.hotInstance.updateSettings({ // enable filtering for all columns filters: true, // enable the column menu for all columns // but display only the 'Filter by value' list and the 'OK' and // 'Cancel' buttons dropdownMenu: { items: { filter_by_value: { // hide the 'Filter by value' list from all columns but the // first one hidden() { return this.getSelectedRangeLast().to.col > 0; }, }, filter_action_bar: { // hide the 'OK' and 'Cancel' buttons from all columns but the // first one hidden() { return this.getSelectedRangeLast().to.col > 0; }, }, }, }, }); ``` ::: ### Filter data programmatically To filter data programmatically, use the [`Filters`](@/api/filters.md) plugin's API. Remember to [enable filtering](#enable-filtering) first. Mind that before you apply new filter conditions, you need to clear the previous ones with [`filters.clearConditions()`](@/api/filters.md#clearconditions). ::: only-for javascript ::: example #exampleFilterThroughAPI1 --html 1 --js 2 --ts 3 @[code](@/content/guides/columns/column-filter/javascript/exampleFilterThroughAPI1.html) @[code](@/content/guides/columns/column-filter/javascript/exampleFilterThroughAPI1.js) @[code](@/content/guides/columns/column-filter/javascript/exampleFilterThroughAPI1.ts) ::: ::: ::: only-for react ::: example #exampleFilterThroughAPI1 :react --js 1 --ts 2 @[code](@/content/guides/columns/column-filter/react/exampleFilterThroughAPI1.jsx) @[code](@/content/guides/columns/column-filter/react/exampleFilterThroughAPI1.tsx) ::: ::: ::: only-for angular ::: example #example11 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-filter/angular/example11.ts) @[code](@/content/guides/columns/column-filter/angular/example11.html) ::: ::: ::: only-for vue ::: example #exampleFilterThroughAPI1 :vue3 @[code](@/content/guides/columns/column-filter/vue/exampleFilterThroughAPI1.vue) ::: ::: ### Combine multiple filter conditions A column can hold more than one condition. The fourth argument of [`filters.addCondition()`](@/api/filters.md#addcondition) sets the logical operation that joins the conditions for that column: - `'conjunction'` -- AND logic. Every condition must match. This is the default, so you can omit the argument. - `'disjunction'` -- OR logic. At least one condition must match. Apply the same operation to all conditions in a column. Don't mix `'conjunction'` and `'disjunction'` for a single column. The following snippet keeps the rows where the price (column index `2`) is less than `200` **or** greater than `400`. ::: only-for javascript ```js const filters = handsontableInstance.getPlugin('filters'); // clear the column's existing conditions first filters.clearConditions(2); // match rows where the price is less than 200 OR greater than 400 filters.addCondition(2, 'lt', [200], 'disjunction'); filters.addCondition(2, 'gt', [400], 'disjunction'); filters.filter(); ``` ::: ::: only-for react ```jsx const filters = hotTableComponentRef.current.hotInstance.getPlugin('filters'); // clear the column's existing conditions first filters.clearConditions(2); // match rows where the price is less than 200 OR greater than 400 filters.addCondition(2, 'lt', [200], 'disjunction'); filters.addCondition(2, 'gt', [400], 'disjunction'); filters.filter(); ``` ::: ::: only-for angular ```ts const filters = this.hotTable.hotInstance!.getPlugin('filters'); // clear the column's existing conditions first filters.clearConditions(2); // match rows where the price is less than 200 OR greater than 400 filters.addCondition(2, 'lt', [200], 'disjunction'); filters.addCondition(2, 'gt', [400], 'disjunction'); filters.filter(); ``` ::: ::: only-for vue ```js const filters = hotTableRef.value.hotInstance.getPlugin('filters'); // clear the column's existing conditions first filters.clearConditions(2); // match rows where the price is less than 200 OR greater than 400 filters.addCondition(2, 'lt', [200], 'disjunction'); filters.addCondition(2, 'gt', [400], 'disjunction'); filters.filter(); ``` ::: The dropdown menu shows at most two conditions plus one **Filter by value** condition per column. If you add more conditions than that, they still filter the data but don't appear in the menu. See [Known limitations](#known-limitations). ### Clear a column filter To clear the conditions for a single column, call [`filters.removeConditions(column)`](@/api/filters.md#removeconditions). This is the programmatic equivalent of selecting the **None** operator in the column menu. ```js const filters = handsontableInstance.getPlugin('filters'); // clear the filter for the column at index 2 filters.removeConditions(2); filters.filter(); ``` To clear the conditions for every column at once, call [`filters.clearConditions()`](@/api/filters.md#clearconditions) without an argument. Adding the `'none'` condition with [`addCondition()`](@/api/filters.md#addcondition) has no filtering effect, because `'none'` matches every row: ```js // matches all rows -- the column stays unfiltered filters.addCondition(2, 'none', []); filters.filter(); ``` ### Save and restore filter settings To persist a user's filter selections (for example, across page reloads or between sessions), export the current conditions with [`filters.exportConditions()`](@/api/filters.md#exportconditions), and store the result. To reapply them later, pass the stored array to [`filters.importConditions()`](@/api/filters.md#importconditions), then call [`filters.filter()`](@/api/filters.md#filter) to apply the change. Unlike [`addCondition()`](@/api/filters.md#addcondition), which takes a visual column index, the `column` property in each exported condition is a physical column index. ::: only-for javascript ::: example #exampleSaveRestoreFilters --html 1 --js 2 --ts 3 @[code](@/content/guides/columns/column-filter/javascript/exampleSaveRestoreFilters.html) @[code](@/content/guides/columns/column-filter/javascript/exampleSaveRestoreFilters.js) @[code](@/content/guides/columns/column-filter/javascript/exampleSaveRestoreFilters.ts) ::: ::: ::: only-for react ::: example #exampleSaveRestoreFilters :react --js 1 --ts 2 @[code](@/content/guides/columns/column-filter/react/exampleSaveRestoreFilters.jsx) @[code](@/content/guides/columns/column-filter/react/exampleSaveRestoreFilters.tsx) ::: ::: ::: only-for angular ::: example #example13 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-filter/angular/example13.ts) @[code](@/content/guides/columns/column-filter/angular/example13.html) ::: ::: ::: only-for vue ::: example #exampleSaveRestoreFilters :vue3 @[code](@/content/guides/columns/column-filter/vue/exampleSaveRestoreFilters.vue) ::: ::: ### Get filtered data After filtering, [`getData()`](@/api/core.md#getdata) returns only the rows that pass the current filters, because filtering removes non-matching rows from the grid's data map instead of merely hiding them. To read every row regardless of the active filters, use [`getSourceData()`](@/api/core.md#getsourcedata). The following demo uses the [`afterFilter()`](@/api/hooks.md#afterfilter) hook to compare the two methods every time the filters change. ::: only-for javascript ::: example #exampleGetFilteredData --html 1 --js 2 --ts 3 @[code](@/content/guides/columns/column-filter/javascript/exampleGetFilteredData.html) @[code](@/content/guides/columns/column-filter/javascript/exampleGetFilteredData.js) @[code](@/content/guides/columns/column-filter/javascript/exampleGetFilteredData.ts) ::: ::: ::: only-for react ::: example #exampleGetFilteredData :react --js 1 --ts 2 @[code](@/content/guides/columns/column-filter/react/exampleGetFilteredData.jsx) @[code](@/content/guides/columns/column-filter/react/exampleGetFilteredData.tsx) ::: ::: ::: only-for angular ::: example #example14 :angular --ts 1 --html 2 @[code](@/content/guides/columns/column-filter/angular/example14.ts) @[code](@/content/guides/columns/column-filter/angular/example14.html) ::: ::: ::: only-for vue ::: example #exampleGetFilteredData :vue3 @[code](@/content/guides/columns/column-filter/vue/exampleGetFilteredData.vue) ::: ::: ## Import the filtering module You can reduce the size of your bundle by importing and registering only the [modules](@/guides/tools-and-building/modules/modules.md) that you need. To use filtering, you need only the following modules: - The [base module](@/guides/tools-and-building/modules/modules.md#import-the-base-module) - The [`Filters`](@/api/filters.md) module - The [`DropdownMenu`](@/api/dropdownMenu.md) module - The [`HiddenRows`](@/api/hiddenRows.md) module - The [`AutoColumnSize`](@/api/autoColumnSize.md) module - The [`CheckboxCellType`](@/guides/cell-types/checkbox-cell-type/checkbox-cell-type.md) module ```js // import the base module import Handsontable from 'handsontable/base'; // import the filtering plugins import { registerPlugin, Filters, DropdownMenu, HiddenRows, AutoColumnSize, } from 'handsontable/plugins'; // import the checkbox cell type import { registerCellType, CheckboxCellType } from 'handsontable/cellTypes'; // register the filtering plugins registerPlugin(Filters); registerPlugin(DropdownMenu); registerPlugin(HiddenRows); registerPlugin(AutoColumnSize); // register the checkbox cell type registerCellType(CheckboxCellType); ``` The order of the `registerPlugin()` calls doesn't matter. Handsontable checks the required modules when you create a Handsontable instance. If a required module isn't registered by then, Handsontable throws an error that lists the missing modules. ## Known limitations At the moment, filtering comes with the following limitations: - There is no easy way to add custom filter operators to the user interface. - The list of values that you can filter by is generated automatically and there's no supported way of modifying it. - The filter's dropdown menu has a limited capacity per column: at most 2 regular conditions and 1 "filter by value" condition. If you add more conditions programmatically via [`addCondition()`](@/api/filters.md), the extra conditions are applied to the data but are not visible or editable in the dropdown menu. ## Navigate the filter menu Use keyboard and pointer interactions together to control the filtering UI: - Press **Tab** and **Shift**+**Tab** to move between filtering components, such as the search input, **Select all**, **Clear all**, condition controls, and action bar controls. - When the search input is focused, press **Arrow up** and **Arrow down** to move through the **Filter by value** list. - When **Select all** or **Clear all** is focused, press **Enter** or **Space** to run the action. - If you hover a non-filter menu item (for example, **Clear column**), the filter-components focus order resets. The next **Tab** focuses the first filter component. For a reference list of filter-related shortcuts, see [Keyboard shortcuts](@/guides/navigation/keyboard-shortcuts/keyboard-shortcuts.md#column-filter-keyboard-shortcuts). ## Related keyboard shortcuts | Windows | macOS | Action | Excel | Sheets | | ----------------------------------- | -------------------------------------- | ----------------- | :-----: | :-----: | | **Alt**+**A** | ⌥+**A** | Clear all filters | ✗ | ✗ | ## API reference For the list of [options](@/guides/getting-started/configuration-options/configuration-options.md), methods, and [Handsontable hooks](@/guides/getting-started/events-and-hooks/events-and-hooks.md) related to filtering, see the following API reference pages: **Plugins**
- [Filters](@/api/filters.md) - [DropdownMenu](@/api/dropdownMenu.md)
## Troubleshooting Didn't find what you need? Try this:
- [View related topics](https://github.com/handsontable/handsontable/labels/Filtering) on GitHub - [Report an issue](https://github.com/handsontable/handsontable/issues/new/choose) on GitHub - [Start a discussion](https://forum.handsontable.com/c/getting-help/questions) on Handsontable's forum - [Contact our technical support](https://handsontable.com/contact?category=technical_support) to get help
--- # Column component URL: https://handsontable.com/docs/react-data-grid/hot-column/ Configure your grid's columns, using the props of the `HotColumn` component. Pass your component as a custom cell editor or a custom cell renderer. [[toc]] ## Declare column settings To declare column-specific settings, pass the settings as `HotColumn` props, exactly as you would with `HotTable`. ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/columns/react-hot-column/react/example1.jsx) @[code](@/content/guides/columns/react-hot-column/react/example1.tsx) ::: ## Object data source When you use object data binding for ``, you need to provide precise information about the data structure for columns. To do so, refer to your object-based data property in `HotColumn`'s `data` prop, for example, ``: ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/columns/react-hot-column/react/example3.jsx) @[code](@/content/guides/columns/react-hot-column/react/example3.tsx) ::: --- # Row headers URL: https://handsontable.com/docs/javascript-data-grid/row-header/ Use default row headers (1, 2, 3), or set them to custom values provided by an array or a function. [[toc]] ## Overview Row headers are gray-colored columns that are used to label each row. By default, these headers are filled with numbers displayed in ascending order. To turn the headers on, set the option [`rowHeaders`](@/api/options.md#rowheaders) to `true`. ## Row headers as an array An array of labels can be used to set the [`rowHeaders`](@/api/options.md#rowheaders) as shown in the example below: ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/rows/row-header/javascript/example2.js) @[code](@/content/guides/rows/row-header/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-header/react/example2.jsx) @[code](@/content/guides/rows/row-header/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-header/angular/example2.ts) @[code](@/content/guides/rows/row-header/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/rows/row-header/vue/example2.vue) ::: ::: ## Row headers as a function The [`rowHeaders`](@/api/options.md#rowheaders) can also be populated using a function as shown in the example below: ::: only-for javascript ::: example #example3 --js 1 --ts 2 @[code](@/content/guides/rows/row-header/javascript/example3.js) @[code](@/content/guides/rows/row-header/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-header/react/example3.jsx) @[code](@/content/guides/rows/row-header/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-header/angular/example3.ts) @[code](@/content/guides/rows/row-header/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/rows/row-header/vue/example3.vue) ::: ::: ## Row header width When you use custom row labels, their content can exceed the default header width. To control the header size, set [`rowHeaderWidth`](@/api/options.md#rowheaderwidth). You can set this option to one of the following: - A number - set the same width for every row header. - An array - set different widths for individual row header levels. The [Row headers as an array](#row-headers-as-an-array) example uses custom labels together with `rowHeaderWidth: 80`. ## Bind rows with headers You can bind row numbers with row headers. This is used mostly to differentiate two business cases in which Handsontable is most often used. 1. When moving a row in a typical grid-like application, the numbers in the row headers remain intact. Only the content is moved. 2. In a data grid, each row has its unique ID. Therefore, the column header should follow its row whenever it changes its position in the grid. ### Basic example To enable the plugin, set the [`bindRowsWithHeaders`](@/api/options.md#bindrowswithheaders) property to `true`. Move the rows in the example below to see what this plugin does. Possible values: - `true` - Enables the plugin. - `strict` - Enables the plugin and the order of indexes is not reorganized after the operation such as hiding or moving rows. - `loose` - Enables the plugin and the order of indexes is re-organized after the operation such as hiding or moving rows. ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/rows/row-header/javascript/example1.js) @[code](@/content/guides/rows/row-header/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-header/react/example1.jsx) @[code](@/content/guides/rows/row-header/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-header/angular/example1.ts) @[code](@/content/guides/rows/row-header/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/rows/row-header/vue/example1.vue) ::: ::: ## Tree grid A tree grid enables you to represent the nested data structures within the data grid. To learn more about this feature, see the [Row parent-child](@/guides/rows/row-parent-child/row-parent-child.md) page. ## Related articles **Related guides**
- [Row parent-child](@/guides/rows/row-parent-child/row-parent-child.md)
**Configuration options**
- [activeHeaderClassName](@/api/options.md#activeheaderclassname) - [currentHeaderClassName](@/api/options.md#currentheaderclassname) - [bindRowsWithHeaders](@/api/options.md#bindrowswithheaders) - [rowHeaders](@/api/options.md#rowheaders) - [rowHeaderWidth](@/api/options.md#rowheaderwidth)
**Core methods**
- [getRowHeader()](@/api/core.md#getrowheader) - [hasRowHeaders()](@/api/core.md#hasrowheaders)
**Hooks**
- [afterGetRowHeader](@/api/hooks.md#aftergetrowheader) - [afterGetRowHeaderRenderers](@/api/hooks.md#aftergetrowheaderrenderers) - [beforeHighlightingRowHeader](@/api/hooks.md#beforehighlightingrowheader) - [modifyRowHeader](@/api/hooks.md#modifyrowheader) - [modifyRowHeaderWidth](@/api/hooks.md#modifyrowheaderwidth)
**Plugins**
- [BindRowsWithHeaders](@/api/bindRowsWithHeaders.md)
--- # Row parent-child URL: https://handsontable.com/docs/javascript-data-grid/row-parent-child/ Reflect the parent-child relationship of your data, using the [`NestedRows`](@/api/nestedRows.md) plugin's interactive UI elements such as expand and collapse buttons or an extended context menu. Handsontable renders this structure as a tree grid. The same pattern is also called a master-detail view or grouping rows. [[toc]] ## Quick setup To enable the [`NestedRows`](@/api/nestedRows.md) plugin, set the [`nestedRows`](@/api/options.md#nestedrows) option to `true`. ::: only-for javascript ```js const hot = new Handsontable(container, { nestedRows: true, }); ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts import {GridSettings, HotTableModule} from '@handsontable/angular-wrapper'; const configurationOptions: GridSettings = { nestedRows: true, }; ``` ```html ``` ::: ::: only-for vue ```ts const hotSettings = { nestedRows: true, }; ``` ::: Note that using all the functionalities provided by the plugin requires enabling the row headers and the Handsontable context menu. To do this set [`rowHeaders`](@/api/options.md#rowheaders) and [`contextMenu`](@/api/options.md#contextmenu) to `true`. The _collapse_ / _expand_ buttons are located in the row headers, and the row modification options _add row_, _insert child_, etc., are in the Context Menu. ## Prepare the data source The data source must have a specific structure to be used with the _Nested Rows_ plugin. The plugin requires the data source to be an array of objects. Each object in the array represents a single _0-level_ entry. _0-level_ refers to an entry, which is not a child of any other entry. If an entry has any child entries, they need to be declared again as an _array of objects_. To assign them to a row, create a `__children` property in the parent element. Child objects can define their own `__children` arrays, so you can nest rows to any depth. Handsontable does not impose a fixed nesting limit -- the depth is determined by your data structure, and row header indentation grows with each level. Here's an example: ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/rows/row-parent-child/javascript/example1.js) @[code](@/content/guides/rows/row-parent-child/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-parent-child/react/example1.jsx) @[code](@/content/guides/rows/row-parent-child/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-parent-child/angular/example1.ts) @[code](@/content/guides/rows/row-parent-child/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/rows/row-parent-child/vue/example1.vue) ::: ::: In the example above, we’ve created a data object consisting of 2016’s Grammy nominees of the “Rock” genre. Each _0-level_ entry declares a category. Under `Best Rock Performance`, nominees are grouped into subcategories (`Major label releases` and `Independent releases`) at the next level, with individual nominees nested one level deeper. The other categories use two levels: category and nominee, assigned under the `__children` properties. Note that the first 0-level object in the array needs to have all columns defined to display the table properly. They can be declared as `null` or an empty string `''`, but they need to be defined. This is optional for the other objects. ### Nested data vs. a flat array A nested rows data source differs from a regular flat array of objects in one respect: child rows live inside their parent's `__children` property, instead of being separate top-level elements. For example, the same three records can be represented either way: ```js // flat array -- three independent top-level rows const flatData = [ { category: 'Best Rock Performance', artist: null }, { category: null, artist: 'Twenty One Pilots' }, { category: null, artist: 'Coldplay' }, ]; // nested rows -- two nominees grouped under one category const nestedData = [ { category: 'Best Rock Performance', artist: null, __children: [ { category: null, artist: 'Twenty One Pilots' }, { category: null, artist: 'Coldplay' }, ], }, ]; ``` [`getSourceData()`](@/api/core.md#getsourcedata) returns this nested structure, `__children` arrays and all. [`getData()`](@/api/core.md#getdata) returns the flattened, currently visible rows -- collapsed child rows are excluded. For more on how Handsontable relates source data to what's displayed, see [Understanding data and indexes](@/guides/getting-started/understanding-data-and-indexes/understanding-data-and-indexes.md). ## User interface The _Nested Rows_ plugin's user interface is placed in the row headers and the Handsontable’s context menu. ### Row headers Each _parent_ row header contains a `+`/`-` button. It is used to collapse or expand its child rows. The child row headers have a bigger indentation, to enable the user to clearly recognize the child and parent elements. In the example above, the `Best Metal Performance` category loads collapsed so you can see the expand/collapse controls right away. ### Context menu The context menu has been extended with a few Nested Rows related options, such as: - Insert child row - Detach from parent The “Insert row above” and “Insert row below” options were modified to work properly with the nested data structure. ## Result After completing this guide, your grid displays rows in a parent-child hierarchy with collapse and expand toggle buttons in row headers and context menu options for inserting and detaching child rows. ## Collapse and expand rows from your code The `NestedRows` plugin lets you collapse and expand parent rows from your own code, and tells you when it happens. ### Methods Get the plugin instance first, from your Handsontable instance: ```js const plugin = hot.getPlugin('nestedRows'); ``` | Method | What it does | | --- | --- | | [`collapseAll()`](@/api/nestedRows.md#collapseall) | Collapses every top-level parent | | [`expandAll()`](@/api/nestedRows.md#expandall) | Expands every parent, at every level | | [`collapseParent(row)`](@/api/nestedRows.md#collapseparent) | Collapses one parent | | [`expandParent(row)`](@/api/nestedRows.md#expandparent) | Expands one parent | | [`toggleParent(row)`](@/api/nestedRows.md#toggleparent) | Collapses an expanded parent, or expands a collapsed one | | [`getCollapsedParents()`](@/api/nestedRows.md#getcollapsedparents) | Physical indexes of the collapsed parents | | [`isParentCollapsed(row)`](@/api/nestedRows.md#isparentcollapsed) | Checks one parent | | [`isParent(row)`](@/api/nestedRows.md#isparent) | Checks whether a row has children | | [`getRowLevel(row)`](@/api/nestedRows.md#getrowlevel) | How deeply a row is nested. Top-level rows are at level `0` | | [`getRowParent(row)`](@/api/nestedRows.md#getrowparent) | The parent of a row | | [`countChildren(row)`](@/api/nestedRows.md#countchildren) | How many children a row has | | [`expandToRow(row)`](@/api/nestedRows.md#expandtorow) | Expands every ancestor, to reveal a hidden row | | [`expandToLevel(level)`](@/api/nestedRows.md#expandtolevel) | Shows rows down to a nesting level, and collapses everything deeper | The example below calls four of them and prints what each one returns. ::: only-for javascript ::: example #example2 --html 1 --js 2 --ts 3 @[code](@/content/guides/rows/row-parent-child/javascript/example2.html) @[code](@/content/guides/rows/row-parent-child/javascript/example2.js) @[code](@/content/guides/rows/row-parent-child/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-parent-child/react/example2.jsx) @[code](@/content/guides/rows/row-parent-child/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-parent-child/angular/example2.ts) @[code](@/content/guides/rows/row-parent-child/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/rows/row-parent-child/vue/example2.vue) ::: ::: ### Which index type to pass Collapsing a parent *trims* its children, which removes them from the grid. A trimmed row has no visual index at all, so the plugin uses two index types: - Methods that act on a row you can see take a **visual** row index. That covers `collapseParent()`, `expandParent()`, `toggleParent()`, `isParentCollapsed()`, `isParent()`, `getRowLevel()`, `getRowParent()`, and `countChildren()`. - Methods that address a row the collapse itself hid take or return a **physical** row index. That covers `getCollapsedParents()` and `expandToRow()`. Convert between the two with [`toVisualRow()`](@/api/core.md#tovisualrow) and [`toPhysicalRow()`](@/api/core.md#tophysicalrow). ### Jump to a row inside a collapsed branch This is where the two index types earn their keep. To reveal a row the user cannot see, you need [`expandToRow()`](@/api/nestedRows.md#expandtorow), and you have to address that row by its **physical** index — a hidden row has no visual index to pass. The example starts fully collapsed. Each button looks up a task's physical row, expands whatever ancestors are hiding it, then selects it. Notice how the physical row stays the same while the visual row changes with whatever else is open. ::: only-for javascript ::: example #example3 --html 1 --js 2 --ts 3 @[code](@/content/guides/rows/row-parent-child/javascript/example3.html) @[code](@/content/guides/rows/row-parent-child/javascript/example3.js) @[code](@/content/guides/rows/row-parent-child/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-parent-child/react/example3.jsx) @[code](@/content/guides/rows/row-parent-child/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-parent-child/angular/example3.ts) @[code](@/content/guides/rows/row-parent-child/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/rows/row-parent-child/vue/example3.vue) ::: ::: ### Hooks Four hooks report every collapse and expand, whether it came from the row header button, the **Enter** shortcut, or one of the methods above: - [`beforeRowCollapse`](@/api/hooks.md#beforerowcollapse) and [`afterRowCollapse`](@/api/hooks.md#afterrowcollapse) - [`beforeRowExpand`](@/api/hooks.md#beforerowexpand) and [`afterRowExpand`](@/api/hooks.md#afterrowexpand) They carry **physical** row indexes. Return `false` from either `before` hook to block the action: ```js const configurationOptions = { // Stop the user from collapsing anything. beforeRowCollapse() { return false; }, }; ``` ### Save and restore the collapsed rows The hooks carry physical indexes, which is what you want to store. To restore the state after replacing the data, collapse the deepest parents first: collapsing a parent hides its children, so a nested parent has to be collapsed while it is still visible. ```js const plugin = hot.getPlugin('nestedRows'); let saved = []; hot.addHook('afterRowCollapse', (currentCollapsedRows, destinationCollapsedRows) => { saved = destinationCollapsedRows; }); hot.addHook('afterRowExpand', (currentCollapsedRows, destinationCollapsedRows) => { saved = destinationCollapsedRows; }); // Later, after replacing the whole data set: hot.loadData(nextDataSet); hot.batchExecution(() => { [...saved] .sort((a, b) => (plugin.getRowLevel(hot.toVisualRow(b)) ?? 0) - (plugin.getRowLevel(hot.toVisualRow(a)) ?? 0)) .forEach((physicalRow) => { const visualRow = hot.toVisualRow(physicalRow); if (visualRow !== null) { plugin.collapseParent(visualRow); } }); }, true); ``` ## Notes ### Known limitations When you use the parent-child row structure, the following Handsontable features are not supported: - [Data source as an array of arrays](@/guides/getting-started/binding-to-data/binding-to-data.md#array-of-arrays) - [Column filter](@/guides/columns/column-filter/column-filter.md) - [Rows sorting](@/guides/rows/rows-sorting/rows-sorting.md) - [Manual row moving via `moveRows()`](@/api/manualRowMove.md#moverows) - use [`dragRows()`](@/api/manualRowMove.md#dragrows) instead When the `NestedRows` plugin is enabled, the `ManualRowMove` plugin's [`moveRows()`](@/api/manualRowMove.md#moverows) method has no effect and logs a console warning. To move rows programmatically, use [`dragRows()`](@/api/manualRowMove.md#dragrows) instead. ### Keyboard shortcuts This header-focused shortcut works only when a row header is focused. Enable [`navigableHeaders: true`](@/api/options.md#navigableheaders) to move focus onto headers with the arrow keys. For more details, see [Keyboard navigation](@/guides/accessibility/accessibility/accessibility.md#keyboard-navigation). | Windows | macOS | Action | Excel | Sheets | | -------------------- | -------------------- | -------------------------------- | :-----: | :-----: | | **Enter** | **Enter** | Collapse or expand the row group | ✗ | ✗ | ## Related articles **Related guides**
- [Row header](@/guides/rows/row-header/row-header.md)
**Configuration options**
- [bindRowsWithHeaders](@/api/options.md#bindrowswithheaders) - [contextMenu](@/api/options.md#contextmenu) - [nestedRows](@/api/options.md#nestedrows) - [rowHeaders](@/api/options.md#rowheaders)
**Core methods**
- [getRowHeader()](@/api/core.md#getrowheader) - [toPhysicalRow()](@/api/core.md#tophysicalrow) - [toVisualRow()](@/api/core.md#tovisualrow)
**Plugin methods**
- [collapseAll()](@/api/nestedRows.md#collapseall) - [collapseParent()](@/api/nestedRows.md#collapseparent) - [countChildren()](@/api/nestedRows.md#countchildren) - [expandAll()](@/api/nestedRows.md#expandall) - [expandParent()](@/api/nestedRows.md#expandparent) - [expandToLevel()](@/api/nestedRows.md#expandtolevel) - [expandToRow()](@/api/nestedRows.md#expandtorow) - [getCollapsedParents()](@/api/nestedRows.md#getcollapsedparents) - [getRowLevel()](@/api/nestedRows.md#getrowlevel) - [getRowParent()](@/api/nestedRows.md#getrowparent) - [isParent()](@/api/nestedRows.md#isparent) - [isParentCollapsed()](@/api/nestedRows.md#isparentcollapsed) - [toggleParent()](@/api/nestedRows.md#toggleparent)
**Hooks**
- [afterAddChild](@/api/hooks.md#afteraddchild) - [afterDetachChild](@/api/hooks.md#afterdetachchild) - [afterRowCollapse](@/api/hooks.md#afterrowcollapse) - [afterRowExpand](@/api/hooks.md#afterrowexpand) - [beforeAddChild](@/api/hooks.md#beforeaddchild) - [beforeDetachChild](@/api/hooks.md#beforedetachchild) - [beforeRowCollapse](@/api/hooks.md#beforerowcollapse) - [beforeRowExpand](@/api/hooks.md#beforerowexpand)
**Plugins**
- [BindRowsWithHeaders](@/api/bindRowsWithHeaders.md) - [ContextMenu](@/api/contextMenu.md) - [NestedRows](@/api/nestedRows.md)
--- # Row hiding URL: https://handsontable.com/docs/javascript-data-grid/row-hiding/ Hide individual rows to avoid rendering them as DOM elements. It helps you reduce screen clutter and improve the grid's performance. [[toc]] ## Overview "Hiding a row" means that the hidden row doesn't get rendered as a DOM element. When you're hiding a row: - The source data doesn't get modified. - The [`HiddenRows`](@/api/hiddenRows.md) plugin doesn't participate in data transformation
(the shape of the data returned by the [`getData*()` methods](@/api/core.md#getdata) stays intact). Hidden rows stay in [`getData()`](@/api/core.md#getdata) and keep their visual index -- unlike [row trimming](@/guides/rows/row-trimming/row-trimming.md), which removes rows from the visual dataset entirely. For a full comparison, see [Understanding data and indexes](@/guides/getting-started/understanding-data-and-indexes/understanding-data-and-indexes.md#how-features-affect-the-two-datasets). ## Enable row hiding To enable row hiding, use the [`hiddenRows`](@/api/options.md#hiddenrows) option. ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/rows/row-hiding/javascript/example1.js) @[code](@/content/guides/rows/row-hiding/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-hiding/react/example1.jsx) @[code](@/content/guides/rows/row-hiding/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-hiding/angular/example1.ts) @[code](@/content/guides/rows/row-hiding/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/rows/row-hiding/vue/example1.vue) ::: ::: ## Set up row hiding To set up your row hiding configuration, follow the steps below. ### Step 1: Specify rows hidden by default To both enable row hiding and specify rows hidden by default, set the [`hiddenRows`](@/api/options.md#hiddenrows) configuration option to an object. In the object, add a `rows` property, and set it to an array of row indexes. Now, those rows are hidden by default: ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/rows/row-hiding/javascript/example2.js) @[code](@/content/guides/rows/row-hiding/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-hiding/react/example2.jsx) @[code](@/content/guides/rows/row-hiding/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-hiding/angular/example2.ts) @[code](@/content/guides/rows/row-hiding/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/rows/row-hiding/vue/example2.vue) ::: ::: ### Step 2: Show UI indicators To easily see which rows are currently hidden, display UI indicators. To enable the UI indicators, in the `hiddenRows` object, set the `indicators` property to `true`: ::: only-for javascript ::: example #example3 --js 1 --ts 2 @[code](@/content/guides/rows/row-hiding/javascript/example3.js) @[code](@/content/guides/rows/row-hiding/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-hiding/react/example3.jsx) @[code](@/content/guides/rows/row-hiding/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-hiding/angular/example3.ts) @[code](@/content/guides/rows/row-hiding/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/rows/row-hiding/vue/example3.vue) ::: ::: ### Step 3: Set up context menu items To easily hide and unhide rows, add row hiding items to Handsontable's [context menu](@/guides/accessories-and-menus/context-menu/context-menu.md). Enable both the [`ContextMenu`](@/api/contextMenu.md) plugin and the [`HiddenRows`](@/api/hiddenRows.md) plugin. Now, the [context menu](@/guides/accessories-and-menus/context-menu/context-menu.md) automatically displays additional items for hiding and unhiding rows. ::: only-for javascript ::: example #example4 --js 1 --ts 2 @[code](@/content/guides/rows/row-hiding/javascript/example4.js) @[code](@/content/guides/rows/row-hiding/javascript/example4.ts) ::: ::: ::: only-for react ::: example #example4 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-hiding/react/example4.jsx) @[code](@/content/guides/rows/row-hiding/react/example4.tsx) ::: ::: ::: only-for angular ::: example #example4 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-hiding/angular/example4.ts) @[code](@/content/guides/rows/row-hiding/angular/example4.html) ::: ::: ::: only-for vue ::: example #example4 :vue3 @[code](@/content/guides/rows/row-hiding/vue/example4.vue) ::: ::: You can also add the row hiding menu items individually, by adding the [`hidden_rows_show`](@/guides/accessories-and-menus/context-menu/context-menu.md#context-menu-with-specific-options) and [`hidden_rows_hide`](@/guides/accessories-and-menus/context-menu/context-menu.md#context-menu-with-specific-options) strings to the `contextMenu` parameter: ::: only-for javascript ::: example #example5 --js 1 --ts 2 @[code](@/content/guides/rows/row-hiding/javascript/example5.js) @[code](@/content/guides/rows/row-hiding/javascript/example5.ts) ::: ::: ::: only-for react ::: example #example5 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-hiding/react/example5.jsx) @[code](@/content/guides/rows/row-hiding/react/example5.tsx) ::: ::: ::: only-for angular ::: example #example5 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-hiding/angular/example5.ts) @[code](@/content/guides/rows/row-hiding/angular/example5.html) ::: ::: ::: only-for vue ::: example #example5 :vue3 @[code](@/content/guides/rows/row-hiding/vue/example5.vue) ::: ::: ### Step 4: Set up copy and paste behavior By default, hidden rows are included in copying and pasting. To exclude hidden rows from copying and pasting, in the `hiddenRows` object, set the `copyPasteEnabled` property to `false`: ::: only-for javascript ::: example #example6 --js 1 --ts 2 @[code](@/content/guides/rows/row-hiding/javascript/example6.js) @[code](@/content/guides/rows/row-hiding/javascript/example6.ts) ::: ::: ::: only-for react ::: example #example6 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-hiding/react/example6.jsx) @[code](@/content/guides/rows/row-hiding/react/example6.tsx) ::: ::: ::: only-for angular ::: example #example6 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-hiding/angular/example6.ts) @[code](@/content/guides/rows/row-hiding/angular/example6.html) ::: ::: ::: only-for vue ::: example #example6 :vue3 @[code](@/content/guides/rows/row-hiding/vue/example6.vue) ::: ::: ## Result After completing this guide, you can hide rows from the grid without changing source data. You can configure default hidden rows, UI indicators, context menu items, and copy-paste behavior. ## Row hiding API methods ::: only-for react ::: tip To use the Handsontable API, you'll need access to the Handsontable instance. You can do that by utilizing a reference to the `HotTable` component, and reading its `hotInstance` property. For more information, see the [Instance methods](@/guides/getting-started/react-methods/react-methods.md) page. ::: ::: For the most popular row hiding tasks, use the API methods below. To see your changes, re-render your Handsontable instance with the [`render()`](@/api/core.md#render) method. ::: only-for angular To obtain the Handsontable instance from the Angular wrapper component, you can use Angular's `@ViewChild` decorator to reference the `HotTableComponent` and then access its `hotInstance` property. ```ts @ViewChild(HotTableComponent, {static: false}) hotTable!: HotTableComponent; ngAfterViewInit() { // The Handsontable instance is available through the hotInstance property const hot = this.hotTable.hotInstance; // You can now use the "hot" instance to call Handsontable's API methods // ... } ``` ::: :::: only-for vue ::: tip To use the Handsontable API, you'll need access to the Handsontable instance. You can do that by utilizing a reference to the `HotTable` component, and reading its `hotInstance` property. For more information, see the [Instance methods](@/guides/getting-started/vue3-hot-reference/vue3-hot-reference.md) page. ::: :::: ### Access the `HiddenRows` plugin instance To access the [`HiddenRows`](@/api/hiddenRows.md) plugin instance, use the [`getPlugin()`](@/api/core.md#getplugin) method: ```js const plugin = hot.getPlugin('hiddenRows'); ``` ### Hide a single row To hide a single row, use the [`hideRow()`](@/api/hiddenRows.md#hiderow) method: ```js const plugin = hot.getPlugin('hiddenRows'); plugin.hideRow(4); ``` ### Hide multiple rows To hide multiple rows: - Either pass row indexes as arguments to the `hideRow()` method - Or pass an array of row indexes to the `hideRows()` method ```js const plugin = hot.getPlugin('hiddenRows'); plugin.hideRow(0, 4, 6); // or plugin.hideRows([0, 4, 6]); ``` ### Unhide a single row To unhide a single row, use the `showRow()` method: ```js const plugin = hot.getPlugin('hiddenRows'); plugin.showRow(4); ``` ### Unhide multiple rows To unhide multiple rows: - Either pass row indexes as arguments to the `showRow()` method - Or pass an array of row indexes to the `showRows()` method ```js const plugin = hot.getPlugin('hiddenRows'); plugin.showRow(0, 4, 6); // or plugin.showRows([0, 4, 6]); ``` ## Related API reference **Configuration options**
- [hiddenRows](@/api/options.md#hiddenrows)
**Hooks**
- [afterHideRows](@/api/hooks.md#afterhiderows) - [afterUnhideRows](@/api/hooks.md#afterunhiderows) - [beforeHideRows](@/api/hooks.md#beforehiderows) - [beforeUnhideRows](@/api/hooks.md#beforeunhiderows)
**Plugins**
- [HiddenRows](@/api/hiddenRows.md)
--- # Row moving URL: https://handsontable.com/docs/javascript-data-grid/row-moving/ Change the order of rows, either manually (dragging them to another location), or programmatically (using Handsontable's API methods). [[toc]] ## Enable the `ManualRowMove` plugin To enable row moving, set the [`manualRowMove`](@/api/options.md#manualrowmove) option to `true`. A draggable move handle appears above the selected row header. You can click and drag it to any location in the row header body. ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/rows/row-moving/javascript/example1.js) @[code](@/content/guides/rows/row-moving/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-moving/react/example1.jsx) @[code](@/content/guides/rows/row-moving/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-moving/angular/example1.ts) @[code](@/content/guides/rows/row-moving/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/rows/row-moving/vue/example1.vue) ::: ::: ## Set a pre-defined row order Instead of setting [`manualRowMove`](@/api/options.md#manualrowmove) to `true`, you can pass an **array of physical row indexes** to define the initial visual order of rows on render. Each position in the array corresponds to a visual (display) position, and the value at that position is the physical (source data) row index. For example: ```js manualRowMove: [2, 0, 1] ``` This renders the rows in the following order: - Visual position 0 → physical row `2` - Visual position 1 → physical row `0` - Visual position 2 → physical row `1` The array must contain all physical row indexes (its length must equal the total number of rows). After the initial render, users can still drag rows to change the order further. ## Data model behavior Moving rows does not reorder your source data array. Handsontable stores the new order as index metadata through its [`IndexMapper`](@/api/indexMapper.md), and leaves the original `sourceData` array untouched. This affects how you read and save the data: - [`getData()`](@/api/core.md#getdata) returns rows in their current visual order, so it reflects any moves. Call it inside the [`afterRowMove`](@/api/hooks.md#afterrowmove) hook to get an order-accurate snapshot to persist. - [`getSourceData()`](@/api/core.md#getsourcedata) returns rows in their original physical order, ignoring any moves. To save the new order after a move, listen to the [`afterRowMove`](@/api/hooks.md#afterrowmove) hook: ```js afterRowMove(movedRows, finalIndex, dropIndex, movePossible, orderChanged) { if (orderChanged) { const reorderedData = this.getData(); // persist reorderedData to your backend } } ``` ### Don't feed the snapshot back into the grid Sending the reordered snapshot back to the grid as its new data applies the move a second time. [`updateData()`](@/api/core.md#updatedata) keeps the current row order on purpose, so Handsontable re-applies the order map it already holds on top of your already-reordered array. One drag then moves the row twice. Treat the snapshot as output only. Send it to your backend, and leave the grid's own data alone. ::: only-for javascript To replace the data and reset the order together, call [`loadData()`](@/api/core.md#loaddata) instead. It clears the row order map along with the data, and it clears the undo history. One exception: if you passed [`manualRowMove`](@/api/options.md#manualrowmove) as an array of indexes, the plugin re-applies that array right after the reset. You get the configured order back, not the source order. ::: :::: only-for react angular vue ### Choose who owns the row order The array you bind to [`data`](@/api/options.md#data) does not change when a user moves a row. The order lives in Handsontable's index map, not in your array. There are two ways to handle that, and you have to stay inside one of them: - **Handsontable owns the order.** You bind the data once, and read the order out when you need it. - **Your app owns the order.** You cancel each move, and reorder your own array instead. Writing [`getData()`](@/api/core.md#getdata) back into the bound `data` mixes the two models. The grid still holds the order map for a move it has already made, so it applies that order on top of your already-reordered array. Your data and the grid end up out of sync, and the row can jump a second time. #### Let Handsontable own the order This is the default. Bind `data` once and leave it alone. Read the current order with [`getData()`](@/api/core.md#getdata) whenever you need to persist it. To start the grid with a non-default order, pass the array through [`initialState`](@/api/options.md#initialstate) rather than [`manualRowMove`](@/api/options.md#manualrowmove). Handsontable reads [`initialState`](@/api/options.md#initialstate) only when it creates the grid, so a re-render can't apply the order a second time: ```js initialState: { manualRowMove: [2, 0, 1], }, ``` The array both enables row moving and sets the starting order, so don't also pass [`manualRowMove`](@/api/options.md#manualrowmove) at the top level. A regular setting takes precedence over the same key in [`initialState`](@/api/options.md#initialstate), so `manualRowMove: true` alongside the code above would discard the order. A `manualRowMove` array passed as a regular option can be re-applied on a later update, which reorders the rows again on top of the order they are already in. How often that happens depends on the framework, so don't rely on it not happening. ::: only-for react For more on this, see [Non-idempotent options](@/guides/getting-started/configuration-options/configuration-options.md#non-idempotent-options). ::: #### Let your app own the order Return `false` from [`beforeRowMove`](@/api/hooks.md#beforerowmove) to cancel Handsontable's move, then apply the same move to your own array. Handsontable keeps its rows in physical order, so your array is the only place the order is stored. In this model you also own the order's history. Reverting a move is your code's job, not the grid's. Cancelling the move changes what the grid does for you, so plan for these: - [`afterRowMove`](@/api/hooks.md#afterrowmove) never fires. The move stops at [`beforeRowMove`](@/api/hooks.md#beforerowmove), before that hook runs, so the snapshot recipe shown earlier on this page does not apply here. Persist the order from your own update instead. - The grid does not re-render or restore the selection, because both wait for a move that actually happened. After the drag, the highlighted row headers stay where they were, and those positions now hold different rows. Re-select the moved rows yourself if that matters. - The hook reports visual row indexes, and the helper below uses them as positions in your array. Those match only while nothing else reorders or hides rows. Add [`columnSorting`](@/api/options.md#columnsorting), [`filters`](@/api/options.md#filters), or trimmed rows, and a visual index no longer points at the same row in your array, so you have to translate the indexes yourself. `finalIndex` is a visual index too. - Cell metadata is keyed by the physical row. Reordering your own array moves the values but not the metadata, so per-row settings such as [`readOnly`](@/api/options.md#readonly), a cell `className`, or a comment stay on the position they were set on and end up on a different row. This helper applies a move to a plain array. `movedRows` holds visual row indexes, and `finalIndex` is the index that the first moved row lands on: ```js function reorderRows(rows, movedRows, finalIndex) { const result = rows.slice(); const moved = movedRows.map(index => rows[index]); // remove from the highest index down, so the lower indexes stay valid movedRows .slice() .sort((a, b) => b - a) .forEach(index => result.splice(index, 1)); result.splice(finalIndex, 0, ...moved); return result; } ``` ::: only-for react Keep the rows in state, and write the new order back from the hook: ```jsx const ExampleComponent = () => { const [rows, setRows] = useState(initialRows); return ( { if (!movePossible) { return; } setRows(prevRows => reorderRows(prevRows, movedRows, finalIndex)); // cancel the grid's own move -- the state update above already applied it return false; }} licenseKey="non-commercial-and-evaluation" /> ); }; ``` ::: ::: only-for angular Keep the rows in a component property, and bind it through the `[data]` input. The grid is created outside Angular's zone, so the hook also runs outside it. Assigning to a bound property there does not start change detection on its own, and the `[data]` input never sees the new array. Re-enter the zone to write the property, and defer it with `setTimeout` so a synchronous write inside the hook can't trigger `NG0100`: ```ts @Component({ selector: 'app-example', template: ``, standalone: true, imports: [HotTableModule], }) export class AppComponent { private readonly ngZone = inject(NgZone); rows = initialRows; readonly hotSettings: GridSettings = { manualRowMove: true, beforeRowMove: (movedRows, finalIndex, dropIndex, movePossible) => { if (!movePossible) { return; } setTimeout(() => { this.ngZone.run(() => { this.rows = reorderRows(this.rows, movedRows, finalIndex); }); }, 0); // cancel the grid's own move -- the assignment above applies it instead return false; }, }; } ``` ::: ::: only-for vue The Vue wrapper reads the bound array by reference, so change that array in place. Assigning a new array to `rows` leaves the grid rendering the old one: ```vue ``` ::: :::: For more on how physical and visual indexes relate, see [Understanding data and indexes](@/guides/getting-started/understanding-data-and-indexes/understanding-data-and-indexes.md). ## Result After completing this guide, you can reorder rows by dragging them with the mouse or by calling `dragRows()` and `moveRows()` programmatically. You can also set a pre-defined row order at initialization. ## API reference ### dragRows vs moveRows There are significant differences between the plugin's [`dragRows`](@/api/manualRowMove.md#dragrows) and [`moveRows`](@/api/manualRowMove.md#moverows) API functions. Both of them change the order of rows, but they rely on different kinds of indexes. The differences between them are shown in the diagrams below. Both of these methods trigger the [`beforeRowMove`](@/api/hooks.md#beforerowmove) and [`afterRowMove`](@/api/hooks.md#afterrowmove) hooks, but only [`dragRows`](@/api/manualRowMove.md#dragrows) passes the `dropIndex` argument to them. The [`dragRows`](@/api/manualRowMove.md#dragrows) method has a `dropIndex` parameter, which points to where the elements are being dropped. ![dragRows method](/img/drag_action.svg) The [`moveRows`](@/api/manualRowMove.md#moverows) method has a `finalIndex` parameter, which points to where the elements will be placed after the _moving_ action - `finalIndex` being the index of the first moved element. ![moveRows method](/img/move_action.svg) The [`moveRows`](@/api/manualRowMove.md#moverows) function cannot perform some actions, e.g., more than one element can't be moved to the last position. In this scenario, the move will be cancelled. The Plugin's [`isMovePossible`](@/api/manualRowMove.md#ismovepossible) API method and the `movePossible` parameters `beforeRowMove` and `afterRowMove` hooks help in determine such situations. The [`moveRows`](@/api/manualRowMove.md#moverows) method is also inactive when the [`NestedRows`](@/api/nestedRows.md) plugin is enabled - see [Row parent-child known limitations](@/guides/rows/row-parent-child/row-parent-child.md#known-limitations). ### Related API reference **Configuration options**
- [manualRowMove](@/api/options.md#manualrowmove)
**Core methods**
- [toVisualRow](@/api/core.md#tovisualrow)
**Hooks**
- [afterRowMove](@/api/hooks.md#afterrowmove) - [beforeRowMove](@/api/hooks.md#beforerowmove)
**Plugins**
- [ManualRowMove](@/api/manualRowMove.md)
--- # Row freezing URL: https://handsontable.com/docs/javascript-data-grid/row-freezing/ Lock the position of specified rows, keeping them visible when scrolling. [[toc]] ## Overview Row freezing locks specific rows of a grid in place, keeping them visible while scrolling to another area of the grid. This feature is sometimes called "pinned rows". ## Example The following example specifies two fixed rows with `fixedRowsTop: 2`. Horizontal scroll bars are needed, so set a container `width` and `overflow: hidden` in CSS. ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/rows/row-freezing/javascript/example1.js) @[code](@/content/guides/rows/row-freezing/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-freezing/react/example1.jsx) @[code](@/content/guides/rows/row-freezing/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-freezing/angular/example1.ts) @[code](@/content/guides/rows/row-freezing/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/rows/row-freezing/vue/example1.vue) ::: ::: ## Freeze rows at the bottom To pin rows to the bottom edge of the grid -- sometimes called footer rows -- use the `fixedRowsBottom` option. The specified number of rows stays visible at the bottom of the viewport while you scroll through the rest of the data. ```js const hot = new Handsontable(container, { data: getData(), // freeze the last two rows as a footer fixedRowsBottom: 2, licenseKey: 'non-commercial-and-evaluation', }); ``` You can combine `fixedRowsTop` and `fixedRowsBottom` to keep both a header and a footer row in view at the same time. ## Result After completing this guide, the rows you specify with `fixedRowsTop` or `fixedRowsBottom` stay visible while you scroll through the rest of the grid. ## Related API reference **Configuration options**
- [fixedRowsBottom](@/api/options.md#fixedrowsbottom) - [fixedRowsTop](@/api/options.md#fixedrowstop)
--- # Row heights URL: https://handsontable.com/docs/javascript-data-grid/row-height/ Configure row heights, using a number, an array or a function. Let your users manually change row heights using Handsontable's interface. [[toc]] ## Overview The default (and minimum) row height is calculated based on the used theme's line height, vertical padding and cell border values and in the `main` theme it's 29px, in the `horizon` it's 37px and in the `classic` styles it's 26 px. Unless configured otherwise, Handsontable assumes that your cell contents fit in this default row height. If your cell contents require heights greater than default (because you use multiline text, or [custom renderers](@/guides/cell-functions/cell-renderer/cell-renderer.md), or custom styles), use one of the following configurations to avoid potential layout problems: - Configure your row heights in advance: set the [`minRowHeights`](@/api/options.md#minrowheights) option to a [number](#set-row-heights-to-a-number), or an [array](#set-row-heights-with-an-array), or a [function](#set-row-heights-with-a-function). This requires you to know the heights beforehand, but results in the best runtime performance. - Set the [`manualRowResize`](@/api/options.md#manualrowresize) option to an array, to configure initial row heights and let your users [adjust the row heights manually](#adjust-row-heights-manually). - Enable the [`AutoRowSize`](@/api/autoRowSize.md) plugin, by setting `autoRowSize: true`. This tells Handsontable to measure the actual row heights in the DOM. It impacts runtime performance but is accurate. Also enable `AutoRowSize` if you call [`scrollViewportTo()`](@/api/core.md#scrollviewportto) -- the method relies on accurate row height measurements to scroll to the correct position. ## Set row heights to a number We set the same height of `40px` for all rows across the entire grid in this example. ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/rows/row-height/javascript/example1.js) @[code](@/content/guides/rows/row-height/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-height/react/example1.jsx) @[code](@/content/guides/rows/row-height/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-height/angular/example1.ts) @[code](@/content/guides/rows/row-height/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/rows/row-height/vue/example1.vue) ::: ::: ## Set row heights with an array In this example, the height is only set for the first rows. Each additional row would be automatically adjusted to the content. ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/rows/row-height/javascript/example2.js) @[code](@/content/guides/rows/row-height/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-height/react/example2.jsx) @[code](@/content/guides/rows/row-height/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-height/angular/example2.ts) @[code](@/content/guides/rows/row-height/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/rows/row-height/vue/example2.vue) ::: ::: ## Set row heights with a function The row height can be set using a function. In this example, the size of all rows is set using a function that takes a row `index` (1, 2 ...) and multiplies it by `20px` for each consecutive row. ::: only-for javascript ::: example #example3 --js 1 --ts 2 @[code](@/content/guides/rows/row-height/javascript/example3.js) @[code](@/content/guides/rows/row-height/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-height/react/example3.jsx) @[code](@/content/guides/rows/row-height/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-height/angular/example3.ts) @[code](@/content/guides/rows/row-height/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/rows/row-height/vue/example3.vue) ::: ::: ## Adjust row heights manually Set the option [`manualRowResize`](@/api/options.md#manualrowresize) to `true` to allow users to manually resize the row height by dragging the handle between the adjacent row headers. Don't forget to enable row headers by setting [`rowHeaders`](@/api/options.md#rowheaders) to `true`. You can adjust the size of one or multiple rows simultaneously, even if the selected rows are not placed next to each other. ::: tip When you set a row height programmatically with [`ManualRowResize#setManualSize()`](@/api/manualRowResize.md#setmanualsize), call `hot.render()` after the method call to repaint the grid. Values below the theme's default row height are stored as the default row height. ::: ::: only-for javascript ::: example #example4 --js 1 --ts 2 @[code](@/content/guides/rows/row-height/javascript/example4.js) @[code](@/content/guides/rows/row-height/javascript/example4.ts) ::: ::: ::: only-for react ::: example #example4 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-height/react/example4.jsx) @[code](@/content/guides/rows/row-height/react/example4.tsx) ::: ::: ::: only-for angular ::: example #example4 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-height/angular/example4.ts) @[code](@/content/guides/rows/row-height/angular/example4.html) ::: ::: ::: only-for vue ::: example #example4 :vue3 @[code](@/content/guides/rows/row-height/vue/example4.vue) ::: ::: ## Related API reference **Configuration options**
- [autoRowSize](@/api/options.md#autorowsize) - [manualRowResize](@/api/options.md#manualrowresize) - [minRowHeights](@/api/options.md#minRowHeights)
**Core methods**
- [getRowHeight()](@/api/core.md#getrowheight) - [scrollViewportTo()](@/api/core.md#scrollviewportto)
**Hooks**
- [afterRowResize](@/api/hooks.md#afterrowresize) - [beforeRowResize](@/api/hooks.md#beforerowresize) - [modifyRowHeight](@/api/hooks.md#modifyrowheight)
**Plugins**
- [AutoRowSize](@/api/autoRowSize.md) - [ManualRowResize](@/api/manualRowResize.md)
--- # Row virtualization URL: https://handsontable.com/docs/javascript-data-grid/row-virtualization/ Render thousands of rows without freezing the browser, using row virtualization. [[toc]] ## Overview Virtualization allows Handsontable to process hundreds of thousands of records without causing the browser to hang. This feature draws only the visible part of the grid, displaying the minimum items physically rendered in the DOM. The elements outside the viewport are rendered when you scroll across the grid. Depending on your configuration, there might be a small offset of columns or rows rendered outside the viewport to make the scrolling performance smoother. This feature is enabled by default and can be turned off by setting the [`renderAllRows`](@/api/options.md#renderallrows) option to `true`. ## Configuring row virtualization You can experiment with the [`viewportRowRenderingOffset`](@/api/options.md#viewportrowrenderingoffset) configuration option, which determines the number of rows displayed outside the visible viewport. If the number passed to that option is greater than the total columns in your data set, then the virtualization will be practically turned off. To make the grid scrollable, set the constant width and height to the same as the container holding Handsontable and set the `overflow` property to `hidden` in the container's stylesheet. If the table contains enough rows or columns, it will be scrollable. The scrolling performance depends mainly on four factors: - Number of cells - number of rows multiplied by the number of columns - Amount and complexity of custom renderers in cells - Number of options enabled in the configuration - Performance of your setup - physical machine and a browser The example below presents a data grid displaying 1 million cells (1000 rows x 1000 columns): ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/rows/row-virtualization/javascript/example1.js) @[code](@/content/guides/rows/row-virtualization/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-virtualization/react/example1.jsx) @[code](@/content/guides/rows/row-virtualization/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-virtualization/angular/example1.ts) @[code](@/content/guides/rows/row-virtualization/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/rows/row-virtualization/vue/example1.vue) ::: ::: ## Known limitations Using row virtualization has the following side effects: - The browser's native search will work only for the visible part of the grid. - Screen readers may announce the wrong total number of rows. Read more in the [Accessibility](@/guides/accessibility/accessibility/accessibility.md#disabling-dom-virtualization-for-improved-accessibility) guide. ## Related articles **Related guides**
- [Column virtualization](@/guides/columns/column-virtualization/column-virtualization.md) - [Performance](@/guides/optimization/performance/performance.md)
**Configuration options**
- [renderAllRows](@/api/options.md#renderallrows) - [viewportRowRenderingOffset](@/api/options.md#viewportrowrenderingoffset)
## Related
- [Performance](@/guides/optimization/performance/performance.md) -- tips for optimizing grid rendering speed with large data sets - [Row height](@/guides/rows/row-height/row-height.md) -- configure fixed or dynamic row heights, which affects viewport calculations during virtualization
--- # Rows sorting URL: https://handsontable.com/docs/javascript-data-grid/rows-sorting/ Sort rows alphabetically or numerically, in ascending, descending, or a custom order, by one or multiple columns. [[toc]] ## Overview Handsontable provides two plugins for sorting rows: - [`ColumnSorting`](@/api/columnSorting.md) -- sorts rows by a **single column** at a time. Clicking a column header cycles through ascending, descending, and unsorted states. - [`MultiColumnSorting`](@/api/multiColumnSorting.md) -- sorts rows by **multiple columns** simultaneously. Hold Ctrl/Cmd and click column headers to add more sort criteria. Sorting runs when you release the mouse button, not when you press it, and only the header label and its sort indicator respond to the click. Pressing the header around them selects the column without sorting it. If the pointer moves while the button is held, Handsontable treats the gesture as a column drag instead of a sort, so [column moving](@/guides/columns/column-moving/column-moving.md) can share the same header. Both plugins sort the view only. The source data array is never modified. To persist the sorted order back to the data source, see [Saving data](@/guides/getting-started/saving-data/saving-data.md). `ColumnSorting` and `MultiColumnSorting` are mutually exclusive. Enable only one at a time. If both options are set to `true`, `ColumnSorting` is automatically disabled. ## Sorting demo Click a column header to sort in ascending (↑) or descending (↓) order. Click again to return to the original order. ::: only-for javascript ::: example #exampleSortingDemo --html 1 --js 2 --ts 3 @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortingDemo.html) @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortingDemo.js) @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortingDemo.ts) ::: ::: ::: only-for react ::: example #exampleSortingDemo :react --js 1 --ts 2 @[code](@/content/guides/rows/rows-sorting/react/exampleSortingDemo.jsx) @[code](@/content/guides/rows/rows-sorting/react/exampleSortingDemo.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/rows/rows-sorting/angular/example1.ts) @[code](@/content/guides/rows/rows-sorting/angular/example1.html) ::: ::: ::: only-for vue ::: example #exampleSortingDemo :vue3 @[code](@/content/guides/rows/rows-sorting/vue/exampleSortingDemo.vue) ::: ::: ## Enable sorting To enable sorting for all columns, set [`columnSorting`](@/api/options.md#columnsorting) to `true`. ::: only-for javascript ```js const configurationOptions = { columnSorting: true, }; ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts import {GridSettings, HotTableModule} from '@handsontable/angular-wrapper'; const configurationOptions: GridSettings = { columnSorting: true, }; ``` ```html ``` ::: ::: tip When `columnSorting` (or `multiColumnSorting`) is enabled, Handsontable adds a sort-order arrow icon to each sortable column header. To make room for the icon, the minimum width of those columns is increased slightly. If you define explicit column widths, the sort icon is placed on top of the existing header space and does not affect your widths. ::: To disable sorting for specific columns, set [`headerAction`](@/api/options.md#columnsorting) to `false` in the per-column configuration. In the following example, only the **Model**, **Date**, and **In stock** columns are sortable. ::: only-for javascript ::: example #exampleEnableSortingForColumns --html 1 --js 2 --ts 3 @[code](@/content/guides/rows/rows-sorting/javascript/exampleEnableSortingForColumns.html) @[code](@/content/guides/rows/rows-sorting/javascript/exampleEnableSortingForColumns.js) @[code](@/content/guides/rows/rows-sorting/javascript/exampleEnableSortingForColumns.ts) ::: ::: ::: only-for react ::: example #exampleEnableSortingForColumns :react --js 1 --ts 2 @[code](@/content/guides/rows/rows-sorting/react/exampleEnableSortingForColumns.jsx) @[code](@/content/guides/rows/rows-sorting/react/exampleEnableSortingForColumns.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/rows/rows-sorting/angular/example2.ts) @[code](@/content/guides/rows/rows-sorting/angular/example2.html) ::: ::: ::: only-for vue ::: example #exampleEnableSortingForColumns :vue3 @[code](@/content/guides/rows/rows-sorting/vue/exampleEnableSortingForColumns.vue) ::: ::: ## Configure sorting Set `columnSorting` to an object to configure the plugin. See the [Reference](#reference) section below for all available options. ::: only-for javascript ```js const configurationOptions = { columnSorting: { // enable click-to-sort on column headers (default: true) headerAction: true, // place empty cells at the end (default: false) sortEmptyCells: false, // show sort-order arrow in the column header (default: true) indicator: true, // sort column 1 descending at initialization initialConfig: { column: 1, sortOrder: 'desc', }, compareFunctionFactory(sortOrder, columnMeta) { return function(value, nextValue) { // return -1, 0, or 1 }; }, }, }; ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts import {GridSettings, HotTableModule} from '@handsontable/angular-wrapper'; const configurationOptions: GridSettings = { columnSorting: { headerAction: true, sortEmptyCells: false, indicator: true, initialConfig: { column: 1, sortOrder: 'desc', }, compareFunctionFactory(sortOrder, columnMeta) { return function(value, nextValue) { // return -1, 0, or 1 }; }, }, }; ``` ```html ``` ::: ::: only-for vue ```ts const hotSettings = { columnSorting: { // enable click-to-sort on column headers (default: true) headerAction: true, // place empty cells at the end (default: false) sortEmptyCells: false, // show sort-order arrow in the column header (default: true) indicator: true, // sort column 1 descending at initialization initialConfig: { column: 1, sortOrder: 'desc', }, compareFunctionFactory(sortOrder, columnMeta) { return function(value, nextValue) { // return -1, 0, or 1 }; }, }, }; ``` ::: You can also override `columnSorting` options per column, using the `columns` configuration: ::: only-for javascript ```js const configurationOptions = { columnSorting: true, columns: [ { columnSorting: { // no sort icon for the first column indicator: false, // disable click-to-sort for the first column headerAction: false, }, }, ], }; ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts import {GridSettings, HotTableModule} from '@handsontable/angular-wrapper'; const configurationOptions: GridSettings = { columnSorting: true, columns: [ { columnSorting: { indicator: false, headerAction: false, }, }, ], }; ``` ```html ``` ::: ::: only-for vue ```ts const hotSettings = { columnSorting: true, columns: [ { columnSorting: { // no sort icon for the first column indicator: false, // disable click-to-sort for the first column headerAction: false, }, }, ], }; ``` ::: ## Sort different types of data Handsontable applies type-aware sorting automatically when you set the [`type`](@/api/options.md#type) option on a column. The supported cell types are: - [`text`](@/guides/cell-types/cell-type/cell-type.md) — sorted alphabetically (default) - [`numeric`](@/guides/cell-types/numeric-cell-type/numeric-cell-type.md) - [`date`](@/guides/cell-types/date-cell-type/date-cell-type.md) - [`time`](@/guides/cell-types/time-cell-type/time-cell-type.md) - [`checkbox`](@/guides/cell-types/checkbox-cell-type/checkbox-cell-type.md) - [`dropdown`](@/guides/cell-types/dropdown-cell-type/dropdown-cell-type.md) - [`autocomplete`](@/guides/cell-types/autocomplete-cell-type/autocomplete-cell-type.md) - [`password`](@/guides/cell-types/password-cell-type/password-cell-type.md) You can also define a custom cell type. See [Cell type](@/guides/cell-types/cell-type/cell-type.md). ::: only-for javascript ::: example #exampleSortDifferentTypes --html 1 --js 2 --ts 3 @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortDifferentTypes.html) @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortDifferentTypes.js) @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortDifferentTypes.ts) ::: ::: ::: only-for react ::: example #exampleSortDifferentTypes :react --js 1 --ts 2 @[code](@/content/guides/rows/rows-sorting/react/exampleSortDifferentTypes.jsx) @[code](@/content/guides/rows/rows-sorting/react/exampleSortDifferentTypes.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/rows/rows-sorting/angular/example3.ts) @[code](@/content/guides/rows/rows-sorting/angular/example3.html) ::: ::: ::: only-for vue ::: example #exampleSortDifferentTypes :vue3 @[code](@/content/guides/rows/rows-sorting/vue/exampleSortDifferentTypes.vue) ::: ::: ## Set an initial sort order Use the `initialConfig` option to apply a sort order when Handsontable initializes. `column` is the visual column index. `sortOrder` is `'asc'` for ascending or `'desc'` for descending. ::: only-for javascript ```js const configurationOptions = { columnSorting: { initialConfig: { column: 0, sortOrder: 'asc', }, }, }; ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts import {GridSettings, HotTableModule} from '@handsontable/angular-wrapper'; const configurationOptions: GridSettings = { columnSorting: { initialConfig: { column: 0, sortOrder: 'asc', }, }, }; ``` ```html ``` ::: ::: only-for vue ```ts const hotSettings = { columnSorting: { initialConfig: { column: 0, sortOrder: 'asc', }, }, }; ``` ::: To set an initial sort order across multiple columns, use the [`MultiColumnSorting`](@/api/multiColumnSorting.md) plugin with an array value for `initialConfig`. See [Set an initial multi-column sort order](#set-an-initial-multi-column-sort-order). ## Add a custom comparator A comparator is a function that determines sort order based on two cell values. Use a custom comparator to implement sorting logic beyond Handsontable's built-in defaults. Common use cases: - Sort by value length, occurrence of a character, or any other custom criterion. - Exclude specific rows from sorting (for example, rows with a particular job title). Use the `compareFunctionFactory` option to provide a comparator factory. The factory receives `sortOrder` (`'asc'` or `'desc'`) and `columnMeta`, and must return a comparator function. The comparator receives two cell values and must return `-1`, `0`, or `1`. ::: only-for javascript ```js const configurationOptions = { columnSorting: { compareFunctionFactory: function(sortOrder, columnMeta) { return function(value, nextValue) { if (value < nextValue) return sortOrder === 'asc' ? -1 : 1; if (value > nextValue) return sortOrder === 'asc' ? 1 : -1; return 0; }; }, }, }; ``` ::: ::: only-for react ```jsx nextValue) return sortOrder === 'asc' ? 1 : -1; return 0; }; }, }} /> ``` ::: ::: only-for angular ```ts import {GridSettings, HotTableModule} from '@handsontable/angular-wrapper'; const configurationOptions: GridSettings = { columnSorting: { compareFunctionFactory: function(sortOrder, columnMeta) { return function(value, nextValue) { if (value < nextValue) return sortOrder === 'asc' ? -1 : 1; if (value > nextValue) return sortOrder === 'asc' ? 1 : -1; return 0; }; }, }, }; ``` ```html ``` ::: ::: only-for vue ```ts const hotSettings = { columnSorting: { compareFunctionFactory: function(sortOrder, columnMeta) { return function(value, nextValue) { if (value < nextValue) return sortOrder === 'asc' ? -1 : 1; if (value > nextValue) return sortOrder === 'asc' ? 1 : -1; return 0; }; }, }, }; ``` ::: ## Use sorting hooks Run code before or after sorting using the following [Handsontable hooks](@/guides/getting-started/events-and-hooks/events-and-hooks.md): - [`beforeColumnSort`](@/api/hooks.md#beforecolumnsort) — fires before sorting. Return `false` to cancel the sort and keep the current order. - [`afterColumnSort`](@/api/hooks.md#aftercolumnsort) — fires after sorting completes. A common use of `beforeColumnSort` is server-side sorting: cancel the client-side sort, send the sort configuration to a server, and reload the data. The following example simulates this: it cancels the front-end sort, "asks a server" to sort the rows, and loads the sorted rows back into the grid. A common use of `afterColumnSort` is excluding specific rows from the sorted result — see the [`afterColumnSort` example](#exclude-rows-from-sorting) in the next section. ::: only-for javascript ::: example #exampleSortingHooks --html 1 --js 2 --ts 3 @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortingHooks.html) @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortingHooks.js) @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortingHooks.ts) ::: ::: ::: only-for react ::: example #exampleSortingHooks :react --js 1 --ts 2 @[code](@/content/guides/rows/rows-sorting/react/exampleSortingHooks.jsx) @[code](@/content/guides/rows/rows-sorting/react/exampleSortingHooks.tsx) ::: ::: ::: only-for angular ::: example #example11 :angular --ts 1 --html 2 @[code](@/content/guides/rows/rows-sorting/angular/example11.ts) @[code](@/content/guides/rows/rows-sorting/angular/example11.html) ::: ::: ::: only-for vue ::: example #exampleSortingHooks :vue3 @[code](@/content/guides/rows/rows-sorting/vue/exampleSortingHooks.vue) ::: ::: ## Exclude rows from sorting You can prevent specific top or bottom rows from being sorted. This is useful when a frozen row at the top displays column labels, or a frozen row at the bottom displays [column summaries](@/guides/columns/column-summary/column-summary.md) — rows that should always stay in place regardless of the sort order. ::: only-for javascript ::: example #exampleExcludeRowsFromSorting --html 1 --js 2 --ts 3 @[code](@/content/guides/rows/rows-sorting/javascript/exampleExcludeRowsFromSorting.html) @[code](@/content/guides/rows/rows-sorting/javascript/exampleExcludeRowsFromSorting.js) @[code](@/content/guides/rows/rows-sorting/javascript/exampleExcludeRowsFromSorting.ts) ::: ::: ::: only-for react ::: example #exampleExcludeRowsFromSorting :react --js 1 --ts 2 @[code](@/content/guides/rows/rows-sorting/react/exampleExcludeRowsFromSorting.jsx) @[code](@/content/guides/rows/rows-sorting/react/exampleExcludeRowsFromSorting.tsx) ::: ::: ::: only-for angular ::: example #example8 :angular --ts 1 --html 2 @[code](@/content/guides/rows/rows-sorting/angular/example8.ts) @[code](@/content/guides/rows/rows-sorting/angular/example8.html) ::: ::: ::: only-for vue ::: example #exampleExcludeRowsFromSorting :vue3 @[code](@/content/guides/rows/rows-sorting/vue/exampleExcludeRowsFromSorting.vue) ::: ::: ## Control sorting programmatically Use the [`ColumnSorting`](@/api/columnSorting.md) plugin API and [`updateSettings()`](@/api/core.md#updatesettings) to control sorting at runtime. This lets you, for example, enable or disable sorting based on conditions, or trigger sorting from outside the grid. ::: only-for react To access Handsontable's API methods from a React component, see [Instance methods](@/guides/getting-started/react-methods/react-methods.md). ::: ### Enable or disable sorting programmatically To enable or disable sorting programmatically, call [`updateSettings()`](@/api/core.md#updatesettings) with `columnSorting` set to `true` or `false`. ::: only-for javascript ```js // enable sorting for all columns handsontableInstance.updateSettings({ columnSorting: true, }); // disable sorting for all columns handsontableInstance.updateSettings({ columnSorting: false, }); // enable sorting on column 0, disable sorting on column 1 handsontableInstance.updateSettings({ columns: [ { columnSorting: { headerAction: true } }, { columnSorting: { headerAction: false } }, ], }); ``` ::: ::: only-for react ```jsx const hotTableComponentRef = useRef(null); // enable sorting for all columns hotTableComponentRef.current.hotInstance.updateSettings({ columnSorting: true, }); // disable sorting for all columns hotTableComponentRef.current.hotInstance.updateSettings({ columnSorting: false, }); // enable sorting on column 0, disable sorting on column 1 hotTableComponentRef.current.hotInstance.updateSettings({ columns: [ { columnSorting: { headerAction: true } }, { columnSorting: { headerAction: false } }, ], }); ``` ::: ::: only-for angular ```ts @ViewChild(HotTableComponent, {static: false}) hotTable!: HotTableComponent; ngAfterViewInit() { const hot = this.hotTable.hotInstance; // enable sorting for all columns hot.updateSettings({ columnSorting: true }); // disable sorting for all columns hot.updateSettings({ columnSorting: false }); // enable sorting on column 0, disable sorting on column 1 hot.updateSettings({ columns: [ { columnSorting: { headerAction: true } }, { columnSorting: { headerAction: false } }, ], }); } ``` ::: ### Sort data programmatically Use [`columnSorting.sort()`](@/api/columnSorting.md#sort) to sort programmatically. Pass an object with `column` (visual column index) and `sortOrder` (`'asc'` or `'desc'`). Each call replaces the previous sort order entirely. Use [`columnSorting.clearSort()`](@/api/columnSorting.md#clearsort) to remove the active sort and return rows to their original order. ::: only-for javascript ```js const columnSorting = handsontableInstance.getPlugin('columnSorting'); // sort column 0 in ascending order columnSorting.sort({ column: 0, sortOrder: 'asc' }); // return rows to their original order columnSorting.clearSort(); ``` ::: ::: only-for react ```jsx const hotTableComponentRef = useRef(null); const columnSorting = hotTableComponentRef.current.hotInstance.getPlugin('columnSorting'); // sort column 0 in ascending order columnSorting.sort({ column: 0, sortOrder: 'asc' }); // return rows to their original order columnSorting.clearSort(); ``` ::: ::: only-for angular ```ts @ViewChild(HotTableComponent, {static: false}) hotTable!: HotTableComponent; ngAfterViewInit() { const columnSorting = this.hotTable.hotInstance.getPlugin('columnSorting'); // sort column 0 in ascending order columnSorting.sort({ column: 0, sortOrder: 'asc' }); // return rows to their original order columnSorting.clearSort(); } ``` ::: To see how it works, try out the following demo: ::: only-for javascript ::: example #exampleSortByAPI --html 1 --js 2 --ts 3 @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortByAPI.html) @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortByAPI.js) @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortByAPI.ts) ::: ::: ::: only-for react ::: example #exampleSortByAPI :react --js 1 --ts 2 @[code](@/content/guides/rows/rows-sorting/react/exampleSortByAPI.jsx) @[code](@/content/guides/rows/rows-sorting/react/exampleSortByAPI.tsx) ::: ::: ::: only-for angular ::: example #example9 :angular --ts 1 --html 2 @[code](@/content/guides/rows/rows-sorting/angular/example9.ts) @[code](@/content/guides/rows/rows-sorting/angular/example9.html) ::: ::: ::: only-for vue ::: example #exampleSortByAPI :vue3 @[code](@/content/guides/rows/rows-sorting/vue/exampleSortByAPI.vue) ::: ::: ## MultiColumnSorting plugin The [`MultiColumnSorting`](@/api/multiColumnSorting.md) plugin extends [`ColumnSorting`](@/api/columnSorting.md) to sort rows by multiple columns at the same time. **Key differences from `ColumnSorting`:** - Sort by multiple columns at once. The column clicked first has the highest sort priority. - Hold **Ctrl**/⌘ and click a column header to add it to the active sort criteria without replacing the existing sort. - Press **Shift**+**Enter** with a column header focused to append that column to the active sort criteria. - `initialConfig` accepts an **array** of sort config objects to define a multi-column initial order. The **Shift**+**Enter** shortcut requires a focused column header. Enable [`navigableHeaders: true`](@/api/options.md#navigableheaders) to move focus onto headers with the arrow keys. `ColumnSorting` and `MultiColumnSorting` are mutually exclusive. If both are set to `true`, `ColumnSorting` is automatically disabled. ### Enable multi-column sorting To enable multi-column sorting for all columns, set [`multiColumnSorting`](@/api/options.md#multicolumnsorting) to `true`. ::: only-for javascript ```js const configurationOptions = { multiColumnSorting: true, }; ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts import {GridSettings, HotTableModule} from '@handsontable/angular-wrapper'; const configurationOptions: GridSettings = { multiColumnSorting: true, }; ``` ```html ``` ::: ### Configure multi-column sorting options `multiColumnSorting` supports the same options as `columnSorting`: `headerAction`, `sortEmptyCells`, `indicator`, and `compareFunctionFactory`. Refer to [Configure sorting](#configure-sorting) for a description of each option. To disable multi-column sorting for a specific column, set `headerAction` to `false` in that column's configuration: ::: only-for javascript ```js const configurationOptions = { multiColumnSorting: true, columns: [ { multiColumnSorting: { headerAction: false, // disable multi-column sorting for the first column }, }, ], }; ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts import {GridSettings, HotTableModule} from '@handsontable/angular-wrapper'; const configurationOptions: GridSettings = { multiColumnSorting: true, columns: [ { multiColumnSorting: { headerAction: false, }, }, ], }; ``` ```html ``` ::: ### Sort by multiple columns To sort by multiple columns interactively, hold **Ctrl**/⌘ and click column headers in the desired priority order. Try the following demo: 1. Click **Brand**. The rows sort by brand. 2. Hold **Ctrl**/⌘ and click **Model**. The rows sort by model within each brand. 3. Hold **Ctrl**/⌘ and click **Price**. The rows sort by price within each model. ::: only-for javascript ::: example #exampleSortByMultipleColumns --html 1 --js 2 --ts 3 @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortByMultipleColumns.html) @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortByMultipleColumns.js) @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortByMultipleColumns.ts) ::: ::: ::: only-for react ::: example #exampleSortByMultipleColumns :react --js 1 --ts 2 @[code](@/content/guides/rows/rows-sorting/react/exampleSortByMultipleColumns.jsx) @[code](@/content/guides/rows/rows-sorting/react/exampleSortByMultipleColumns.tsx) ::: ::: ::: only-for angular ::: example #example4 :angular --ts 1 --html 2 @[code](@/content/guides/rows/rows-sorting/angular/example4.ts) @[code](@/content/guides/rows/rows-sorting/angular/example4.html) ::: ::: ::: only-for vue ::: example #exampleSortByMultipleColumns :vue3 @[code](@/content/guides/rows/rows-sorting/vue/exampleSortByMultipleColumns.vue) ::: ::: ### Set an initial multi-column sort order Use `initialConfig` with an **array** of sort config objects to apply a multi-column sort at initialization. Each object has a `column` property (visual column index) and a `sortOrder` property (`'asc'` or `'desc'`). The array order determines sort priority: the first entry has the highest priority. In the following demo, the data is initially sorted by **Brand** ascending, then by **Model** descending: ::: only-for javascript ::: example #exampleInitialSortOrder --html 1 --js 2 --ts 3 @[code](@/content/guides/rows/rows-sorting/javascript/exampleInitialSortOrder.html) @[code](@/content/guides/rows/rows-sorting/javascript/exampleInitialSortOrder.js) @[code](@/content/guides/rows/rows-sorting/javascript/exampleInitialSortOrder.ts) ::: ::: ::: only-for react ::: example #exampleInitialSortOrder :react --js 1 --ts 2 @[code](@/content/guides/rows/rows-sorting/react/exampleInitialSortOrder.jsx) @[code](@/content/guides/rows/rows-sorting/react/exampleInitialSortOrder.tsx) ::: ::: ::: only-for angular ::: example #example5 :angular --ts 1 --html 2 @[code](@/content/guides/rows/rows-sorting/angular/example5.ts) @[code](@/content/guides/rows/rows-sorting/angular/example5.html) ::: ::: ::: only-for vue ::: example #exampleInitialSortOrder :vue3 @[code](@/content/guides/rows/rows-sorting/vue/exampleInitialSortOrder.vue) ::: ::: ::: only-for javascript ```js const configurationOptions = { multiColumnSorting: { initialConfig: [ { column: 0, sortOrder: 'asc' }, // sort column 0 ascending (highest priority) { column: 1, sortOrder: 'desc' }, // sort column 1 descending (next priority) ], }, }; ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts import {GridSettings, HotTableModule} from '@handsontable/angular-wrapper'; const configurationOptions: GridSettings = { multiColumnSorting: { initialConfig: [ { column: 0, sortOrder: 'asc' }, { column: 1, sortOrder: 'desc' }, ], }, }; ``` ```html ``` ::: ### Sort by multiple columns programmatically Use [`multiColumnSorting.sort()`](@/api/multiColumnSorting.md#sort) to sort by multiple columns programmatically. Pass an array of sort config objects. The array order determines sort priority. Each call replaces the previous sort order entirely. Use [`multiColumnSorting.clearSort()`](@/api/multiColumnSorting.md#clearsort) to remove all sort criteria and return rows to their original order. ::: only-for javascript ```js const multiColumnSorting = handsontableInstance.getPlugin('multiColumnSorting'); // sort column 0 ascending, then column 1 descending multiColumnSorting.sort([ { column: 0, sortOrder: 'asc' }, { column: 1, sortOrder: 'desc' }, ]); // return rows to their original order multiColumnSorting.clearSort(); ``` ::: ::: only-for react ```jsx const hotTableComponentRef = useRef(null); const multiColumnSorting = hotTableComponentRef.current.hotInstance.getPlugin('multiColumnSorting'); // sort column 0 ascending, then column 1 descending multiColumnSorting.sort([ { column: 0, sortOrder: 'asc' }, { column: 1, sortOrder: 'desc' }, ]); // return rows to their original order multiColumnSorting.clearSort(); ``` ::: ::: only-for angular ```ts @ViewChild(HotTableComponent, {static: false}) hotTable!: HotTableComponent; ngAfterViewInit() { const multiColumnSorting = this.hotTable.hotInstance.getPlugin('multiColumnSorting'); // sort column 0 ascending, then column 1 descending multiColumnSorting.sort([ { column: 0, sortOrder: 'asc' }, { column: 1, sortOrder: 'desc' }, ]); // return rows to their original order multiColumnSorting.clearSort(); } ``` ::: To see how it works, try out the following demo: ::: only-for javascript ::: example #exampleSortByAPIMultipleColumns --html 1 --js 2 --ts 3 @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortByAPIMultipleColumns.html) @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortByAPIMultipleColumns.js) @[code](@/content/guides/rows/rows-sorting/javascript/exampleSortByAPIMultipleColumns.ts) ::: ::: ::: only-for react ::: example #exampleSortByAPIMultipleColumns :react --js 1 --ts 2 @[code](@/content/guides/rows/rows-sorting/react/exampleSortByAPIMultipleColumns.jsx) @[code](@/content/guides/rows/rows-sorting/react/exampleSortByAPIMultipleColumns.tsx) ::: ::: ::: only-for angular ::: example #example10 :angular --ts 1 --html 2 @[code](@/content/guides/rows/rows-sorting/angular/example10.ts) @[code](@/content/guides/rows/rows-sorting/angular/example10.html) ::: ::: ::: only-for vue ::: example #exampleSortByAPIMultipleColumns :vue3 @[code](@/content/guides/rows/rows-sorting/vue/exampleSortByAPIMultipleColumns.vue) ::: ::: ## Add custom sort icons The default sort icons (↑↓) are rendered using CSS `-webkit-mask-image`. Override the following pseudo-elements to replace them: - `.columnSorting.sortAction.ascending::before` - `.columnSorting.sortAction.descending::before` ::: only-for javascript ::: example #exampleCustomSortIcons --html 1 --js 2 --ts 3 --css 4 @[code](@/content/guides/rows/rows-sorting/javascript/exampleCustomSortIcons.html) @[code](@/content/guides/rows/rows-sorting/javascript/exampleCustomSortIcons.js) @[code](@/content/guides/rows/rows-sorting/javascript/exampleCustomSortIcons.ts) @[code](@/content/guides/rows/rows-sorting/javascript/exampleCustomSortIcons.css) ::: ::: ::: only-for react ::: example #exampleCustomSortIcons :react --js 1 --css 2 --ts 3 @[code](@/content/guides/rows/rows-sorting/react/exampleCustomSortIcons.jsx) @[code](@/content/guides/rows/rows-sorting/react/exampleCustomSortIcons.css) @[code](@/content/guides/rows/rows-sorting/react/exampleCustomSortIcons.tsx) ::: ::: ::: only-for angular ::: example #example6 :angular --ts 1 --html 2 @[code](@/content/guides/rows/rows-sorting/angular/example6.ts) @[code](@/content/guides/rows/rows-sorting/angular/example6.html) ::: ::: ::: only-for vue ::: example #exampleCustomSortIcons :vue3 --css 2 @[code](@/content/guides/rows/rows-sorting/vue/exampleCustomSortIcons.vue) @[code](@/content/guides/rows/rows-sorting/javascript/exampleCustomSortIcons.css) ::: ::: To replace the column-priority number indicators used by the `MultiColumnSorting` plugin (1, 2, etc.), override the `content` of `.columnSorting.sort-1::after` and subsequent pseudo-elements: ::: only-for javascript ::: example #exampleCustomSortIcons3 --html 1 --js 2 --ts 3 --css 4 @[code](@/content/guides/rows/rows-sorting/javascript/exampleCustomSortIcons3.html) @[code](@/content/guides/rows/rows-sorting/javascript/exampleCustomSortIcons3.js) @[code](@/content/guides/rows/rows-sorting/javascript/exampleCustomSortIcons3.ts) @[code](@/content/guides/rows/rows-sorting/javascript/exampleCustomSortIcons3.css) ::: ::: ::: only-for react ::: example #exampleCustomSortIcons3 :react --js 1 --css 2 --ts 3 @[code](@/content/guides/rows/rows-sorting/react/exampleCustomSortIcons3.jsx) @[code](@/content/guides/rows/rows-sorting/react/exampleCustomSortIcons3.css) @[code](@/content/guides/rows/rows-sorting/react/exampleCustomSortIcons3.tsx) ::: ::: ::: only-for angular ::: example #example7 :angular --ts 1 --html 2 @[code](@/content/guides/rows/rows-sorting/angular/example7.ts) @[code](@/content/guides/rows/rows-sorting/angular/example7.html) ::: ::: ::: only-for vue ::: example #exampleCustomSortIcons3 :vue3 --css 2 @[code](@/content/guides/rows/rows-sorting/vue/exampleCustomSortIcons3.vue) @[code](@/content/guides/rows/rows-sorting/javascript/exampleCustomSortIcons3.css) ::: ::: ## Import the sorting module To reduce bundle size, import only the modules you need. For sorting, you need the [base module](@/guides/tools-and-building/modules/modules.md#import-the-base-module) and the plugin module. ```js // import the base module import Handsontable from 'handsontable/base'; // import ColumnSorting (or MultiColumnSorting instead) import { registerPlugin, ColumnSorting } from 'handsontable/plugins'; // register the plugin registerPlugin(ColumnSorting); ``` ## Result After completing this guide, users can sort rows by clicking column headers, and you can control sort order programmatically. You can use `ColumnSorting` for single-column sorting or `MultiColumnSorting` for multi-column sorting with custom priority. ## Related keyboard shortcuts These header-focused shortcuts work only when a column header is focused. Enable [`navigableHeaders: true`](@/api/options.md#navigableheaders) to move focus onto headers with the arrow keys. For more details, see [Keyboard navigation](@/guides/accessibility/accessibility/accessibility.md#keyboard-navigation). | Windows | macOS | Action | Excel | Sheets | | --- | --- | --- | :---: | :---: | | **Enter** | **Enter** | Sort by the focused column, cycling through ascending, descending, and original order | ✗ | ✗ | | **Shift**+**Enter** | ⇧+**Enter** | Append the focused column to the active sort criteria. Requires the [`MultiColumnSorting`](@/api/multiColumnSorting.md) plugin. | ✗ | ✗ | ## Reference ### Plugin comparison | Feature | `ColumnSorting` | `MultiColumnSorting` | | --- | --- | --- | | Sort by single column | Yes | Yes | | Sort by multiple columns | No | Yes | | Ctrl/Cmd + click to add columns | No | Yes | | `initialConfig` type | Object | Array of objects | | Mutual exclusivity | Disabled when `MultiColumnSorting` is enabled | Disables `ColumnSorting` | ### Configuration options | Option | Type | Default | Description | | --- | --- | --- | --- | | `headerAction` | `boolean` | `true` | When `true`, clicking a column header sorts by that column. | | `sortEmptyCells` | `boolean` | `false` | When `true`, empty cells participate in sorting. When `false`, empty cells are always placed at the end. | | `indicator` | `boolean` | `true` | When `true`, a sort-order arrow icon is shown in the column header. | | `compareFunctionFactory` | `function` | -- | A factory that returns a custom comparator function. See [Add a custom comparator](#add-a-custom-comparator). | | `initialConfig` | `object` | -- | Sort config applied at initialization. Contains `column` (visual index) and `sortOrder` (`'asc'` or `'desc'`). | ## API reference For the full list of options, methods, and hooks related to sorting, see the following API reference pages: - [`ColumnSorting`](@/api/columnSorting.md) - [`MultiColumnSorting`](@/api/multiColumnSorting.md) ## Troubleshooting Didn't find what you need? Try this:
- [View related topics](https://github.com/handsontable/handsontable/labels/Column%20sorting) on GitHub - [Report an issue](https://github.com/handsontable/handsontable/issues/new/choose) on GitHub - [Start a discussion](https://forum.handsontable.com/c/getting-help/questions) on Handsontable's forum - [Contact our technical support](https://handsontable.com/contact?category=technical_support) to get help
--- # Rows pagination URL: https://handsontable.com/docs/javascript-data-grid/rows-pagination/ Added in Handsontable 16.1.0. Split large data sets into pages to improve usability and rendering performance. Pagination operates fully on the client side and automatically recomputes the total page count whenever rows are added, removed, filtered, or otherwise modified. [[toc]] ## Pagination demo Use the controls below the grid to switch between pages. ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code collapse={8-107, 117-162}](@/content/guides/rows/rows-pagination/javascript/example1.js) @[code collapse={8-107, 117-162}](@/content/guides/rows/rows-pagination/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code collapse={8-107, 126-156}](@/content/guides/rows/rows-pagination/react/example1.jsx) @[code collapse={8-107, 126-156}](@/content/guides/rows/rows-pagination/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code collapse={16-115, 122-168}](@/content/guides/rows/rows-pagination/angular/example1.ts) @[code](@/content/guides/rows/rows-pagination/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/rows/rows-pagination/vue/example1.vue) ::: ::: ## Enable pagination To enable pagination set the [`pagination`](@/api/options.md#pagination) option to `true`. ```javascript const configurationOptions = { pagination: true, }; ``` Defining the option as `true` is equivalent to defining the following options: ```javascript const configurationOptions = { pagination: { pageSize: 10, pageSizeList: ['auto', 5, 10, 20, 50, 100], initialPage: 1, showPageSize: true, showCounter: true, showNavigation: true, uiContainer: null, }, }; ``` ## Configure pagination You can customize pagination using available settings, such as showing or hiding specific UI sections like the page size selector, page counter, or page navigation. Control aspects such as the number of rows per page, the initial page on load, or whether the grid should automatically adjust page size based on the container's height. You can configure the following options: ```javascript const configurationOptions = { pagination: { // Set number of rows per page. If the value is `auto` then the // page size is calculated // based on available height. pageSize: 20, // Provide a list of selectable page sizes pageSizeList: ['auto', 10, 20, 50], // Set the initial page when the grid loads initialPage: 2, // Show or hide the page size section showPageSize: false, // Show or hide the page counter section showCounter: true, // Show or hide the page navigation section showNavigation: true, // Custom container where the pagination UI will be injected // (optional) uiContainer: null, } }; ``` In the data grid below, several pagination options are applied to provide a customized experience. The demo allows you to resize the table container and observe how the auto page size feature (`pageSize: 'auto'`) dynamically adjusts the number of visible rows to fit the available space. ::: only-for javascript ::: example #example2 --html 1 --js 2 --ts 3 @[code](@/content/guides/rows/rows-pagination/javascript/example2.html) @[code collapse={8-107, 124-169}](@/content/guides/rows/rows-pagination/javascript/example2.js) @[code collapse={8-107, 124-169}](@/content/guides/rows/rows-pagination/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --css 2 --ts 3 @[code collapse={8-107, 137-167}](@/content/guides/rows/rows-pagination/react/example2.jsx) @[code](@/content/guides/rows/rows-pagination/react/example2.css) @[code collapse={8-107, 137-167}](@/content/guides/rows/rows-pagination/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code collapse={16-115, 142-188}](@/content/guides/rows/rows-pagination/angular/example2.ts) @[code](@/content/guides/rows/rows-pagination/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/rows/rows-pagination/vue/example2.vue) ::: ::: ## Control pagination programmatically Build your own pagination UI using API methods such as [`setPage()`](@/api/pagination.md#setpage), [`nextPage()`](@/api/pagination.md#nextpage), [`prevPage()`](@/api/pagination.md#prevpage), and more. For a complete list of available methods and hooks, see the [`Pagination`](@/api/pagination.md) plugin API reference. ::: only-for javascript ::: example #example3 --html 1 --js 2 --ts 3 @[code](@/content/guides/rows/rows-pagination/javascript/example3.html) @[code collapse={8-107, 122-167}](@/content/guides/rows/rows-pagination/javascript/example3.js) @[code collapse={8-107, 123-168}](@/content/guides/rows/rows-pagination/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --css 2 --ts 3 @[code collapse={8-107, 200-230}](@/content/guides/rows/rows-pagination/react/example3.jsx) @[code](@/content/guides/rows/rows-pagination/react/example3.css) @[code collapse={8-107, 200-230}](@/content/guides/rows/rows-pagination/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code collapse={42-141, 154-200}](@/content/guides/rows/rows-pagination/angular/example3.ts) @[code](@/content/guides/rows/rows-pagination/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/rows/rows-pagination/vue/example3.vue) ::: ::: ## Choose where to display the pagination UI By default, the pagination UI is displayed at the bottom of the grid. You can change the place of the pagination component by setting the [`uiContainer`](@/api/options.md#uicontainer) option to a custom container element. ::: only-for javascript ::: example #example4 --html 1 --js 2 --ts 3 @[code](@/content/guides/rows/rows-pagination/javascript/example4.html) @[code collapse={8-107, 119-164}](@/content/guides/rows/rows-pagination/javascript/example4.js) @[code collapse={8-107, 119-164}](@/content/guides/rows/rows-pagination/javascript/example4.ts) ::: ::: ::: only-for react ::: example #example4 :react --js 1 --css 2 --ts 3 @[code collapse={8-107, 140-170}](@/content/guides/rows/rows-pagination/react/example4.jsx) @[code](@/content/guides/rows/rows-pagination/react/example4.css) @[code collapse={8-107, 140-170}](@/content/guides/rows/rows-pagination/react/example4.tsx) ::: ::: ::: only-for angular ::: example #example4 :angular --ts 1 --html 2 @[code collapse={16-115, 144-190}](@/content/guides/rows/rows-pagination/angular/example4.ts) @[code](@/content/guides/rows/rows-pagination/angular/example4.html) ::: ::: ::: only-for vue ::: example #example4 :vue3 @[code](@/content/guides/rows/rows-pagination/vue/example4.vue) ::: ::: ## Modify paged data Sometimes you need to modify data only on the currently visible page. Core method like [`setDataAtCell`](@/api/core.md#setdataatcell) operates on all rows, including those hidden by pagination. To modify data only on the current page, you can use the [`getPaginationData`](@/api/pagination.md#getpaginationdata) method to get the pagination state and use it in conjunction with Core method. ::: only-for javascript ::: example #example5 --html 1 --js 2 --ts 3 @[code](@/content/guides/rows/rows-pagination/javascript/example5.html) @[code collapse={8-107, 116-161}](@/content/guides/rows/rows-pagination/javascript/example5.js) @[code collapse={8-107, 117-162}](@/content/guides/rows/rows-pagination/javascript/example5.ts) ::: ::: ::: only-for react ::: example #example5 :react --js 1 --css 2 --ts 3 @[code collapse={8-107, 158-188}](@/content/guides/rows/rows-pagination/react/example5.jsx) @[code](@/content/guides/rows/rows-pagination/react/example5.css) @[code collapse={8-107, 159-189}](@/content/guides/rows/rows-pagination/react/example5.tsx) ::: ::: ::: only-for angular ::: example #example5 :angular --ts 1 --html 2 @[code collapse={25-124, 131-177}](@/content/guides/rows/rows-pagination/angular/example5.ts) @[code](@/content/guides/rows/rows-pagination/angular/example5.html) ::: ::: ::: only-for vue ::: example #example5 :vue3 @[code](@/content/guides/rows/rows-pagination/vue/example5.vue) ::: ::: ## Use pagination hooks You can run your code before or after different stages of pagination, using the following [Handsontable hooks](@/guides/getting-started/events-and-hooks/events-and-hooks.md): - [`beforePageChange()`](@/api/hooks.md#beforepagechange) - [`afterPageChange()`](@/api/hooks.md#afterpagechange) - [`beforePageSizeChange()`](@/api/hooks.md#beforepagesizechange) - [`afterPageSizeChange()`](@/api/hooks.md#afterpagesizechange) - [`afterPageSizeVisibilityChange()`](@/api/hooks.md#afterpagesizevisibilitychange) - [`afterPageCounterVisibilityChange()`](@/api/hooks.md#afterpagecountervisibilitychange) - [`afterPageNavigationVisibilityChange()`](@/api/hooks.md#afterpagenavigationvisibilitychange) ::: only-for javascript ```js const configurationOptions = { beforePageChange() { // add your code here return false; // to block page change }, afterPageChange() { // add your code here }, beforePageSizeChange() { // add your code here return false; // to block page size change }, // ... }; ``` ::: ::: only-for react ```jsx { // add your code here return false; // to block page change }} afterPageChange={() => { // add your code here }} beforePageSizeChange={() => { // add your code here return false; // to block page size change }} // ... /> ``` ::: ::: only-for vue ```ts const hotSettings = { beforePageChange() { // add your code here return false; // to block page change }, afterPageChange() { // add your code here }, beforePageSizeChange() { // add your code here return false; // to block page size change }, // ... }; ``` ::: ## Localize pagination Translate default pagination labels - such as "Page size:", "Page" and more - using the global translations mechanism. The pagination introduces the following keys to the language dictionary that you can use to translate the pagination UI: - `PAGINATION_SECTION = 'Pagination'` - `PAGINATION_PAGE_SIZE_SECTION = 'Page size'` - `PAGINATION_PAGE_SIZE_AUTO = 'Auto'` - `PAGINATION_COUNTER_SECTION = '[start] - [end] of [total]'` - `PAGINATION_NAV_SECTION = 'Page [currentPage] of [totalPages]'` - `PAGINATION_FIRST_PAGE = 'Go to first page'` - `PAGINATION_PREV_PAGE = 'Go to previous page'` - `PAGINATION_NEXT_PAGE = 'Go to next page'` - `PAGINATION_LAST_PAGE = 'Go to last page'` To learn more about the translation mechanism, see the [Languages guide](@/guides/internationalization/language/language.md). The example below demonstrates how to customize the translation of the pagination counter and navigation sections. ::: only-for javascript ::: example #example6 --js 1 --ts 2 @[code collapse={8-107, 129-174}](@/content/guides/rows/rows-pagination/javascript/example6.js) @[code collapse={8-107, 129-174}](@/content/guides/rows/rows-pagination/javascript/example6.ts) ::: ::: ::: only-for react ::: example #example6 :react --js 1 --ts 2 @[code collapse={8-107, 139-169}](@/content/guides/rows/rows-pagination/react/example6.jsx) @[code collapse={8-107, 139-169}](@/content/guides/rows/rows-pagination/react/example6.tsx) ::: ::: ::: only-for angular ::: example #example6 :angular --ts 1 --html 2 @[code collapse={16-115, 139-185}](@/content/guides/rows/rows-pagination/angular/example6.ts) @[code](@/content/guides/rows/rows-pagination/angular/example6.html) ::: ::: ::: only-for vue ::: example #example6 :vue3 @[code](@/content/guides/rows/rows-pagination/vue/example6.vue) ::: ::: ## Customize pagination UI You can customize the look of each pagination element by assigning new values to the CSS variables defined in our main, horizon and classic themes, aligning them with your app's design guidelines. For a list of available variables, see the [Handsontable Design System](@/guides/styling/design-system/design-system.md) on Figma. Within the plugin the following CSS variables are available: - `--ht-pagination-bar-foreground-color`: Controls the text color of pagination bar elements; - `--ht-pagination-bar-background-color`: Sets the background color of the pagination bar; - `--ht-pagination-bar-horizontal-padding`: Defines the left and right padding of the pagination bar; - `--ht-pagination-bar-vertical-padding`: Controls the top and bottom padding of the pagination bar; ## Importing the pagination module You can reduce the size of your bundle by importing and using only the [modules](@/guides/tools-and-building/modules/modules.md) that you need. To use pagination, you need only the following modules: - The [base module](@/guides/tools-and-building/modules/modules.md#import-the-base-module) - The [`Pagination`](@/api/pagination.md) module ```js // import the base module import Handsontable from 'handsontable/base'; // import the Pagination plugin import { registerPlugin, Pagination } from 'handsontable/plugins'; // register the Pagination plugin registerPlugin(Pagination); ``` ## Known limitations When pagination is enabled: - [`fixedRowsTop`](@/api/options.md#fixedrowstop) and [`fixedRowsBottom`](@/api/options.md#fixedrowsbottom) options should be disabled. - [`NestedRows`](@/api/nestedRows.md) and [`MergeCells`](@/api/mergeCells.md) plugins should be disabled. - The [`height`](@/api/options.md#height) option set as `auto` is not supported when the `pageSize: 'auto'` is set. - Pagination always displays a fixed number of rows per page (default is `10`), regardless of data changes such as hiding, trimming, filtering, removing, adding, or pasting rows - unless `pageSize: 'auto'` is set. ## Result After completing this guide, your grid divides rows into pages with built-in navigation controls. You can configure the page size, customize the UI position, control pagination programmatically, and react to page changes using hooks. ## Related blog articles
- [Handsontable 16.1: Row Pagination, Loading Plugin, and Long-Term Support Policy](https://handsontable.com/blog/handsontable-16.1-row-pagination-loading-plugin-and-long-term-support-policy)
## Related API reference **Configuration options**
- [pagination](@/api/options.md#pagination) - [fixedRowsBottom](@/api/options.md#fixedrowsbottom) - [fixedRowsTop](@/api/options.md#fixedrowstop)
**Hooks**
- [beforePageChange()](@/api/hooks.md#beforepagechange) - [afterPageChange()](@/api/hooks.md#afterpagechange) - [beforePageSizeChange()](@/api/hooks.md#beforepagesizechange) - [afterPageSizeChange()](@/api/hooks.md#afterpagesizechange) - [afterPageSizeVisibilityChange()](@/api/hooks.md#afterpagesizevisibilitychange) - [afterPageCounterVisibilityChange()](@/api/hooks.md#afterpagecountervisibilitychange) - [afterPageNavigationVisibilityChange()](@/api/hooks.md#afterpagenavigationvisibilitychange)
**Plugins**
- [Pagination](@/api/pagination.md) - [NestedRows](@/api/nestedRows.md) - [MergeCells](@/api/mergeCells.md)
## Troubleshooting Didn't find what you need? Try this:
- [View related topics](https://github.com/handsontable/handsontable/labels/Pagination) on GitHub - [Report an issue](https://github.com/handsontable/handsontable/issues/new/choose) on GitHub - [Start a discussion](https://forum.handsontable.com/c/getting-help/questions) on Handsontable's forum - [Contact our technical support](https://handsontable.com/contact?category=technical_support) to get help
--- # Row trimming URL: https://handsontable.com/docs/javascript-data-grid/row-trimming/ Hide individual rows from your interface and exclude them from the rendering process and `DataMap`. [[toc]] ## Overview Row trimming is similar to [row hiding](@/guides/rows/row-hiding/row-hiding.md), but works a bit differently. The [`TrimRows`](@/api/trimRows.md) plugin allows the trimming of specific rows from the table. Rows being trimmed aren't rendered and aren't included in the `DataMap`, which can be retrieved by calling the [`getData`](@/api/core.md#getdata) method. Unlike hidden rows, trimmed rows are removed from the visual dataset entirely, so rows after them shift to a lower visual index. For a full comparison, see [Understanding data and indexes](@/guides/getting-started/understanding-data-and-indexes/understanding-data-and-indexes.md#how-features-affect-the-two-datasets). ## Setup To enable row trimming, set the [`trimRows`](@/api/options.md#trimrows) option to `true`. ::: only-for javascript ```js // enable the `TrimRows` plugin trimRows: true, ``` ::: ::: only-for react ```jsx // enable the `TrimRows` plugin ``` ::: ::: only-for angular ```ts // enable the `TrimRows` plugin trimRows: true, ``` ::: ::: only-for vue ```ts // enable the `TrimRows` plugin trimRows: true, ``` ::: To both enable row trimming and trim selected rows at Handsontable's initialization, set the [`trimRows`](@/api/options.md#trimrows) option to an array of physical row indexes. ::: only-for javascript ```js // enable the `TrimRows` plugin // at Handsontable's initialization, trim rows 5, 10, and 15 trimRows: [5, 10, 15], ``` ::: ::: only-for react ```jsx // enable the `TrimRows` plugin // at Handsontable's initialization, trim rows 5, 10, and 15 ``` ::: ::: only-for angular ```ts // enable the `TrimRows` plugin // at Handsontable's initialization, trim rows 5, 10, and 15 trimRows: [5, 10, 15], ``` ::: ::: only-for vue ```ts // enable the `TrimRows` plugin // at Handsontable's initialization, trim rows 5, 10, and 15 trimRows: [5, 10, 15], ``` ::: ## Example Note that the second, third, and sixth rows are missing in the following example: ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/rows/row-trimming/javascript/example1.js) @[code](@/content/guides/rows/row-trimming/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-trimming/react/example1.jsx) @[code](@/content/guides/rows/row-trimming/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-trimming/angular/example1.ts) @[code](@/content/guides/rows/row-trimming/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/rows/row-trimming/vue/example1.vue) ::: ::: ## Result After completing this guide, rows you specify are excluded from rendering and from `DataMap`. You can trim rows at initialization or dynamically at runtime using the plugin API. ## Related API ### Plugin methods ::: only-for react ::: tip To use the Handsontable API, you'll need access to the Handsontable instance. You can do that by utilizing a reference to the `HotTable` component, and reading its `hotIntance` property. For more information, see the [Instance methods](@/guides/getting-started/react-methods/react-methods.md) page. ::: ::: ::: only-for angular To obtain the Handsontable instance from the Angular wrapper component, you can use Angular's `@ViewChild` decorator to reference the `HotTableComponent` and then access its `hotInstance` property. ```ts @ViewChild(HotTableComponent, {static: false}) hotTable!: HotTableComponent; ngAfterViewInit() { // The Handsontable instance is available through the hotInstance property const hot = this.hotTable.hotInstance; // You can now use the "hot" instance to call Handsontable's API methods // ... } ``` ::: ::: only-for vue ::: tip To use the Handsontable API, you'll need access to the Handsontable instance. You can do that by utilizing a reference to the `HotTable` component, and reading its `hotInstance` property. For more information, see the [Instance methods](@/guides/getting-started/vue3-hot-reference/vue3-hot-reference.md) page. ::: ::: The plugin instance can be accessed by calling: ```js const plugin = hot.getPlugin('trimRows'); ``` To trim a single row, call the [`trimRow()`](@/api/trimRows.md#trimrow) method of the plugin object: ```js plugin.trimRow(4); ``` To trim multiple rows, either pass them as arguments to the [`trimRow()`](@/api/trimRows.md#trimrow) method, or pass an array of physical row indexes to the [`trimRows()`](@/api/trimRows.md#trimrows) method: ```js plugin.trimRow(0, 4, 6); // or plugin.trimRows([0, 4, 6]); ``` To restore the trimmed row(s), use the following methods: ```js plugin.untrimRow(4); ``` ```js plugin.untrimRow(0, 4, 6); ``` ```js plugin.untrimRows([0, 4, 6]); ``` To see the changes made, call `hot.render();` to re-render the table. ### API reference **Options**
- [trimRows](@/api/options.md#trimrows)
**Hooks**
- [afterTrimRow](@/api/hooks.md#aftertrimrow) - [afterUntrimRow](@/api/hooks.md#afteruntrimrow) - [beforeTrimRow](@/api/hooks.md#beforetrimrow) - [beforeUntrimRow](@/api/hooks.md#beforeuntrimrow)
**Plugins**
- [TrimRows](@/api/trimRows.md)
--- # Row pre-populating URL: https://handsontable.com/docs/javascript-data-grid/row-prepopulating/ Pre-populate new rows with default values when users add rows to the grid. [[toc]] ## Basic spare rows To keep one empty row at the bottom of the grid, set [`minSpareRows`](@/api/options.md#minsparerows) to `1`. ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/rows/row-prepopulating/javascript/example1.js) @[code](@/content/guides/rows/row-prepopulating/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-prepopulating/react/example1.jsx) @[code](@/content/guides/rows/row-prepopulating/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-prepopulating/angular/example1.ts) @[code](@/content/guides/rows/row-prepopulating/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/rows/row-prepopulating/vue/example1.vue) ::: ::: ## Spare rows with placeholder styling To hint what to enter in the spare row, add a custom cell renderer that displays greyed-out placeholder text in empty cells. The renderer checks whether the whole row is empty, then shows a template value in a lighter color. ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/rows/row-prepopulating/javascript/example2.js) @[code](@/content/guides/rows/row-prepopulating/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-prepopulating/react/example2.jsx) @[code](@/content/guides/rows/row-prepopulating/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-prepopulating/angular/example2.ts) @[code](@/content/guides/rows/row-prepopulating/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/rows/row-prepopulating/vue/example2.vue) ::: ::: ## Auto-populating with template values For full pre-population, use the [`beforeChange`](@/api/hooks.md#beforechange) hook to fill all cells in a spare row with template values the moment the user starts editing. The `isEmptyRow()` helper detects whether the row is untouched, and the hook pushes changes for every column except the one the user is editing. ::: only-for javascript ::: example #example3 --js 1 --ts 2 @[code](@/content/guides/rows/row-prepopulating/javascript/example3.js) @[code](@/content/guides/rows/row-prepopulating/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/rows/row-prepopulating/react/example3.jsx) @[code](@/content/guides/rows/row-prepopulating/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/rows/row-prepopulating/angular/example3.ts) @[code](@/content/guides/rows/row-prepopulating/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/rows/row-prepopulating/vue/example3.vue) ::: ::: ## Pre-populate from an adjacent row You can copy values from the row above when the user inserts a new row. Use the [`afterCreateRow`](@/api/hooks.md#aftercreaterow) hook to read the source row's data and write it to the newly created row. ```js const hot = new Handsontable(container, { data: [ ['Product A', 10, 'active'], ['Product B', 20, 'inactive'], ], colHeaders: ['Name', 'Quantity', 'Status'], afterCreateRow(index) { if (index > 0) { const sourceRow = hot.getSourceDataAtRow(index - 1); sourceRow.forEach((value, col) => { hot.setDataAtCell(index, col, value); }); } }, licenseKey: 'non-commercial-and-evaluation', }); ``` ## Pre-populate from a server-fetched default You can fetch default row values from a server and apply them when the user adds a new row. Use the [`afterCreateRow`](@/api/hooks.md#aftercreaterow) hook to trigger the request and populate the row once the response arrives. ```js const hot = new Handsontable(container, { data: [], colHeaders: ['Name', 'Quantity', 'Status'], afterCreateRow(index) { fetch('/api/row-defaults') .then((response) => response.json()) .then((defaults) => { hot.setDataAtRow(index, [defaults.name, defaults.quantity, defaults.status]); }); }, licenseKey: 'non-commercial-and-evaluation', }); ``` ## Result Your grid keeps one or more empty rows at the bottom. Depending on the approach, spare rows show greyed-out placeholder text or auto-fill all cells with template values when the user starts editing. ## Related API reference **Hooks**
- [afterCreateRow](@/api/hooks.md#aftercreaterow) - [beforeChange](@/api/hooks.md#beforechange)
--- # Selection URL: https://handsontable.com/docs/javascript-data-grid/selection/ Select a single cell, a range of adjacent cells, or multiple non-adjacent ranges of cells. [[toc]] Use the selection API to control how users select cells -- single cells, ranges, columns, or rows -- and to read or set selections programmatically. ## Overview Selection enables you to select a single cell or ranges of cells within Handsontable. Once selected, you can retrieve data from the cell, edit the cell's contents, or change the style of the cell. ## Basic configuration With this feature, you can select single cells or ranges of cells across a grid. Easily retrieve the coordinates of the selected cells to clear or change the cells' content. Use ⌘ on Mac or **Ctrl** on Windows to select non-adjacent ranges of cells. Click a column header to select all cells in that column. Click a row header to select all cells in that row. Both require [`colHeaders`](@/api/options.md#colheaders) or [`rowHeaders`](@/api/options.md#rowheaders) to be enabled. ## Select ranges There are different modes in which you can use this plugin. Choose between selecting a single cell, a range of adjacent cells, and multiple ranges of non-contiguous cells. Possible values of [`selectionMode`](@/api/options.md#selectionmode): - [`single`](@/api/options.md#selectionmode) - You can select a single cell. - [`range`](@/api/options.md#selectionmode) - You can select multiple cells within a single rangeselected. - [`multiple`](@/api/options.md#selectionmode) - Multiple non-contiguous ranges of cells can be selected. ::: only-for javascript ::: example #example1 --html 1 --js 2 --ts 3 --css 4 @[code](@/content/guides/cell-features/selection/javascript/example1.html) @[code](@/content/guides/cell-features/selection/javascript/example1.js) @[code](@/content/guides/cell-features/selection/javascript/example1.ts) @[code](@/content/guides/cell-features/selection/javascript/example1.css) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/selection/react/example1.jsx) @[code](@/content/guides/cell-features/selection/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/selection/angular/example1.ts) @[code](@/content/guides/cell-features/selection/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/cell-features/selection/vue/example1.vue) ::: ::: ## Get data from the selected ranges To retrieve the selected cells as an array of arrays, you use the [`getSelected()`](@/api/core.md#getselected) or [`getSelectedRange()`](@/api/core.md#getselectedrange) methods. ::: only-for javascript ::: example #example2 --html 1 --js 2 --ts 3 @[code](@/content/guides/cell-features/selection/javascript/example2.html) @[code](@/content/guides/cell-features/selection/javascript/example2.js) @[code](@/content/guides/cell-features/selection/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/selection/react/example2.jsx) @[code](@/content/guides/cell-features/selection/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/selection/angular/example2.ts) @[code](@/content/guides/cell-features/selection/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/cell-features/selection/vue/example2.vue) ::: ::: ## Modify the selected cells You may want to delete, format, or otherwise change the selected cells. For example, you can change a value or add CSS classes to the selected cells using the demo below. ::: only-for javascript ::: example #example3 --html 1 --css 2 --js 3 --ts 4 @[code](@/content/guides/cell-features/selection/javascript/example3.html) @[code](@/content/guides/cell-features/selection/javascript/example3.css) @[code](@/content/guides/cell-features/selection/javascript/example3.js) @[code](@/content/guides/cell-features/selection/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --css 1 --js 2 --ts 3 @[code](@/content/guides/cell-features/selection/react/example3.css) @[code](@/content/guides/cell-features/selection/react/example3.jsx) @[code](@/content/guides/cell-features/selection/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/selection/angular/example3.ts) @[code](@/content/guides/cell-features/selection/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/cell-features/selection/vue/example3.vue) ::: ::: ## Style the selection area You can change the background color of selected cells using CSS. The base selection color is defined in the `.area` class. When using multiple non-adjacent selections (**Cmd**/**Ctrl** + click), each additional selection layer receives a numbered class: `area-1` for the second layer, `area-2` for the third, and so on. Each class is cumulative — a cell in the second layer has both `area` and `area-1`. The example below customizes the color of each selection layer using these CSS classes. ::: only-for javascript ::: example #example4 --html 1 --css 2 --js 3 --ts 4 @[code](@/content/guides/cell-features/selection/javascript/example4.html) @[code](@/content/guides/cell-features/selection/javascript/example4.css) @[code](@/content/guides/cell-features/selection/javascript/example4.js) @[code](@/content/guides/cell-features/selection/javascript/example4.ts) ::: ::: ::: only-for react ::: example #example4 :react --css 1 --js 2 --ts 3 @[code](@/content/guides/cell-features/selection/react/example4.css) @[code](@/content/guides/cell-features/selection/react/example4.jsx) @[code](@/content/guides/cell-features/selection/react/example4.tsx) ::: ::: ::: only-for angular ::: example #example4 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/selection/angular/example4.ts) @[code](@/content/guides/cell-features/selection/angular/example4.html) ::: ::: ::: only-for vue ::: example #example4 :vue3 @[code](@/content/guides/cell-features/selection/vue/example4.vue) ::: ::: Unfortunately, there is no easy way to change the border color of the selection. ### Add a class name to selected cells Use the [`afterDrawSelection`](@/api/hooks.md#afterdrawselection) hook to give selected cells your own class name. Handsontable calls the hook for each cell of the selection area while it draws the selection. If the hook returns a string, Handsontable adds that string as a class name to the cell. If the hook returns anything else, the cell gets no extra class. The hook receives the visual row and column index of the cell, the corners of the current selection layer, and the number of the layer being drawn. ## Resize a selection with handles When you set [`selectionHandles`](@/api/options.md#selectionhandles) to `true`, hovering over a selected range shows a pill-shaped handle at the midpoint of each edge. Drag a handle to move that edge and resize the selection. The handles adjust the selected area only -- they do not move, fill, or change any cell data. To enable selection handles: ```javascript selectionHandles: true, ``` This option applies at the grid level and defaults to `false`. **Limitations:** - Handles appear on desktop only. Touch devices keep their existing native selection handles and are not affected by this option. - The option has no effect when [`selectionMode`](@/api/options.md#selectionmode) is `'single'`. - Handles are not shown for full-row, full-column, or select-all selections. - A handle is not shown on an edge that is flush with the grid boundary or that lands on a frozen-pane line ([`fixedRowsTop`](@/api/options.md#fixedrowstop), [`fixedRowsBottom`](@/api/options.md#fixedrowsbottom), [`fixedColumnsStart`](@/api/options.md#fixedcolumnsstart)). - The handles are pointer-only -- there is no keyboard equivalent. To resize a selection with the keyboard, use Shift+Arrow. ::: only-for javascript ::: example #example7 --html 1 --js 2 --ts 3 @[code](@/content/guides/cell-features/selection/javascript/example7.html) @[code](@/content/guides/cell-features/selection/javascript/example7.js) @[code](@/content/guides/cell-features/selection/javascript/example7.ts) ::: ::: ::: only-for react ::: example #example7 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/selection/react/example7.jsx) @[code](@/content/guides/cell-features/selection/react/example7.tsx) ::: ::: ::: only-for angular ::: example #example7 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/selection/angular/example7.ts) @[code](@/content/guides/cell-features/selection/angular/example7.html) ::: ::: ::: only-for vue ::: example #example7 :vue3 @[code](@/content/guides/cell-features/selection/vue/example7.vue) ::: ::: ## Move a selection by dragging When you set [`moveCells`](@/api/options.md#movecells) to `true`, you can drag the border of a selected cell range to move or copy the range's data to a new location. For configuration details, limitations, hooks, and a live demo, see the [Move cells](@/guides/cell-features/move-cells/move-cells.md) guide. To move whole rows or columns instead, see the [Row moving](@/guides/rows/row-moving/row-moving.md) and [Column moving](@/guides/columns/column-moving/column-moving.md) guides. ## Select cells programmatically Use [`selectCell()`](@/api/core.md#selectcell) to select a single cell or a range of cells from code. Pass the start and end row/column indices to define a range. Use [`deselectCell()`](@/api/core.md#deselectcell) to clear the selection. ::: only-for javascript ::: example #example5 --html 1 --js 2 --ts 3 @[code](@/content/guides/cell-features/selection/javascript/example5.html) @[code](@/content/guides/cell-features/selection/javascript/example5.js) @[code](@/content/guides/cell-features/selection/javascript/example5.ts) ::: ::: ::: only-for react ::: example #example5 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/selection/react/example5.jsx) @[code](@/content/guides/cell-features/selection/react/example5.tsx) ::: ::: ::: only-for angular ::: example #example5 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/selection/angular/example5.ts) @[code](@/content/guides/cell-features/selection/angular/example5.html) ::: ::: ::: only-for vue ::: example #example5 :vue3 @[code](@/content/guides/cell-features/selection/vue/example5.vue) ::: ::: ## Jump across the grid's edges When you use keyboard navigation to cross an edge of the grid, you can set cell selection to jump to the opposite edge. #### Jump across vertical edges To enable jumping across the left and right edges: - Set the [`autoWrapRow`](@/api/options.md#autowraprow) configuration option to `true`. To jump across a vertical edge: - When cell selection is on a row's first cell, press **←**. - When cell selection is on a row's last cell, press **→**, or press **Tab**. #### Jump across horizontal edges To enable jumping across the top and bottom edges: - Set the [`autoWrapCol`](@/api/options.md#autowrapcol) configuration option to `true`. To jump across a horizontal edge: - When cell selection is on a column's first cell, press **↑**. - When cell selection is on a column's last cell, press **↓**, or press **Enter**. Use the checkboxes in the demo below to toggle `autoWrapRow` and `autoWrapCol`, then select a cell and use the keyboard to test the wrap-around behavior at the grid's edges. ::: only-for javascript ::: example #example6 --html 1 --js 2 --ts 3 @[code](@/content/guides/cell-features/selection/javascript/example6.html) @[code](@/content/guides/cell-features/selection/javascript/example6.js) @[code](@/content/guides/cell-features/selection/javascript/example6.ts) ::: ::: ::: only-for react ::: example #example6 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/selection/react/example6.jsx) @[code](@/content/guides/cell-features/selection/react/example6.tsx) ::: ::: ::: only-for angular ::: example #example6 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/selection/angular/example6.ts) @[code](@/content/guides/cell-features/selection/angular/example6.html) ::: ::: ::: only-for vue ::: example #example6 :vue3 @[code](@/content/guides/cell-features/selection/vue/example6.vue) ::: ::: ## Result Users can select cells using the configured mode -- single cell, range, or multiple ranges. Programmatic selections take effect immediately and fire the relevant selection hooks. ## Related keyboard shortcuts | Windows | macOS | Action | Excel | Sheets | | ------------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------- | :-----: | :-----: | | **Ctrl**+**A** | ⌘+**A** | Select all cells | ✓ | ✓ | |**Ctrl**+**Shift**+**Space** |⌘+⇧+**Space** | Select all cells and headers | ✓ | ✓ | | **Ctrl**+**Space** | ⌃+**Space** | Select the entire column | ✓ | ✓ | | **Shift**+**Space** | ⇧+**Space** | Select the entire row | ✓ | ✓ | | **Ctrl**+**Shift**+**↑** | ⌘+⇧+**↑** | Extend the selection to the first cell of the current column** | ✓ | ✓ | | **Ctrl**+**Shift**+**↓** | ⌘+⇧+**↓** | Extend the selection to the last cell of the current column** | ✓ | ✓ | | **Ctrl**+**Shift**+**←** | ⌘+⇧+**←** | Extend the selection to the leftmost cell of the current row** | ✓ | ✓ | | **Ctrl**+**Shift**+**→** | ⌘+⇧+**→** | Extend the selection to the rightmost cell of the current row** | ✓ | ✓ | | **Shift** + Arrow keys | ⇧ + Arrow keys | Extend the selection by one cell | ✓ | ✓ | | **Shift**+**Home** | ⇧+**Home** | Extend the selection to the first non-frozen cell of the current row* | ✓ | ✗ | | **Shift**+**End** | ⇧+**End** | Extend the selection to the last non-frozen cell of the current row* | ✗ | ✗ | | **Shift**+**Page Up** | ⇧+**Page Up** | Extend the selection by one screen up | ✓ | ✓ | | **Shift**+**Page Down** | ⇧+**Page Down** | Extend the selection by one screen down | ✓ | ✓ | | **Ctrl**+**Enter** | ⌘+**Enter** | Fill the selected range of cells with the value of the active cell | ✗ | ✓ | | **Delete** | **Delete** | Clear the contents of the selected cells | ✓ | ✓ | | **Backspace** | **Backspace** | Clear the contents of the selected cells | ✓ | ✓ | * This action depends on your [layout direction](@/guides/internationalization/layout-direction/layout-direction.md).
** In case of multiple selection layers, only the last selection layer gets extended. ## Related API reference **Configuration options**
- [autoWrapCol](@/api/options.md#autowrapcol) - [autoWrapRow](@/api/options.md#autowraprow) - [fragmentSelection](@/api/options.md#fragmentselection) - [disableVisualSelection](@/api/options.md#disablevisualselection) - [dragToScroll](@/api/options.md#dragtoscroll) - [moveCells](@/api/options.md#movecells) - [selectionHandles](@/api/options.md#selectionhandles) - [selectionMode](@/api/options.md#selectionmode) - [outsideClickDeselects](@/api/options.md#outsideclickdeselects)
**Core methods**
- [deselectCell()](@/api/core.md#deselectcell) - [getSelected()](@/api/core.md#getselected) - [getSelectedLast()](@/api/core.md#getselectedlast) - [getSelectedRange()](@/api/core.md#getselectedrange) - [getSelectedRangeLast()](@/api/core.md#getselectedrangelast) - [selectAll()](@/api/core.md#selectall) - [selectCell()](@/api/core.md#selectcell) - [selectCells()](@/api/core.md#selectcells) - [selectColumns()](@/api/core.md#selectcolumns) - [selectRows()](@/api/core.md#selectrows)
**Hooks**
- [afterMoveCells](@/api/hooks.md#aftermovecells) - [afterDeselect](@/api/hooks.md#afterdeselect) - [afterDrawSelection](@/api/hooks.md#afterdrawselection) - [afterModifyTransformEnd](@/api/hooks.md#aftermodifytransformend) - [afterModifyTransformStart](@/api/hooks.md#aftermodifytransformstart) - [afterSelection](@/api/hooks.md#afterselection) - [afterSelectionByProp](@/api/hooks.md#afterselectionbyprop) - [afterSelectionEnd](@/api/hooks.md#afterselectionend) - [afterSelectionEndByProp](@/api/hooks.md#afterselectionendbyprop) - [beforeMoveCells](@/api/hooks.md#beforemovecells) - [afterOnSelectionHandleMouseDown](@/api/hooks.md#afteronselectionhandlemousedown) - [modifyTransformStart](@/api/hooks.md#modifytransformstart)
**Plugins**
- [DragToScroll](@/api/dragToScroll.md)
Microsoft and Excel are registered trademarks of Microsoft Corporation. Google Sheets is a trademark of Google LLC. --- # Move cells URL: https://handsontable.com/docs/javascript-data-grid/move-cells/ Added in Handsontable 18.1.0. Move or copy a selected range of cells by dragging its border, the same way you would in a spreadsheet application. [[toc]] ## Overview When you set [`moveCells`](@/api/options.md#movecells) to `true`, hovering the border of a selected cell range shows a grab cursor. Dragging that border moves the block's data -- cell values and formatting -- to the new location. Hold **Ctrl** (Windows) or ⌘ (Mac) during the drag to copy instead of move. Press **Escape** to cancel a drag before releasing. The `moveCells` option was introduced in Handsontable 18.1.0. To move whole rows or columns instead of a cell range, see the [Row moving](@/guides/rows/row-moving/row-moving.md) and [Column moving](@/guides/columns/column-moving/column-moving.md) guides. ## Enable cell moving To enable drag-to-move, set the [`moveCells`](@/api/options.md#movecells) option to `true`: ```javascript moveCells: true, ``` This option applies at the grid level and defaults to `false`. Select a range of cells in the demo below, then drag its border to a new location. Hold **Ctrl** or ⌘ to copy the range instead of moving it. ::: only-for javascript ::: example #example1 --html 1 --js 2 --ts 3 @[code](@/content/guides/cell-features/move-cells/javascript/example1.html) @[code](@/content/guides/cell-features/move-cells/javascript/example1.js) @[code](@/content/guides/cell-features/move-cells/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/move-cells/react/example1.jsx) @[code](@/content/guides/cell-features/move-cells/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/move-cells/angular/example1.ts) @[code](@/content/guides/cell-features/move-cells/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/cell-features/move-cells/vue/example1.vue) ::: ::: ## Moving cells with formulas When the [`formulas`](@/api/options.md#formulas) plugin is active, formula references adjust automatically on move -- the same way they do in Excel. ## Limitations - Drag-to-move works on a single contiguous cell range only. It has no effect on full-row, full-column, select-all, or multiple selections. - The target must stay within the grid. Neither the target nor the source may overlap read-only cells, because a move has to clear the source. Copying with **Ctrl** or ⌘ leaves the source in place, so a read-only source cell blocks a move but not a copy. - Drag-to-move is hidden when [`disableVisualSelection`](@/api/options.md#disablevisualselection) is set. - A move that would split a merged cell is blocked. ## Hooks - [`beforeMoveCells`](@/api/hooks.md#beforemovecells) fires before the data relocates. Return `false` from the handler to cancel the move. - [`afterMoveCells`](@/api/hooks.md#aftermovecells) fires after the data has been relocated. ## Move cells programmatically To move or copy a range from code, call the plugin's `moveCellRange()` method: ```javascript hot.getPlugin('moveCells').moveCellRange(sourceRange, targetTopLeft, isCopy); ``` ## Related articles **Related guides**
- [Selection](@/guides/cell-features/selection/selection.md) - [Row moving](@/guides/rows/row-moving/row-moving.md) - [Column moving](@/guides/columns/column-moving/column-moving.md)
**Configuration options**
- [moveCells](@/api/options.md#movecells) - [disableVisualSelection](@/api/options.md#disablevisualselection)
**Hooks**
- [afterMoveCells](@/api/hooks.md#aftermovecells) - [beforeMoveCells](@/api/hooks.md#beforemovecells)
**Plugins**
- [MoveCells](@/api/moveCells.md)
Microsoft and Excel are registered trademarks of Microsoft Corporation. --- # Merge cells URL: https://handsontable.com/docs/javascript-data-grid/merge-cells/ Merge adjacent cells, using the **Ctrl**+**M** shortcut or the context menu. Control merged cells, using Handsontable's API. [[toc]] ## Overview By merging, you can combine two or more adjacent cells into a single cell that spans several rows or columns. Handsontable merges cells in the same way as Microsoft Excel: keeps only the upper-left value of the selected range and clears other values. This clearing happens in the underlying data, not just on screen -- see [Effect on the underlying data](#effect-on-the-underlying-data). ## How to merge cells To enable the merge cells feature, set the [`mergeCells`](@/api/options.md#mergecells) option to `true` or to an array. To initialize Handsontable with predefined merged cells, provide merged cells details in form of an array: The `row` and `col` properties use visual indexes. They refer to positions as displayed in the grid, in the same coordinate space as `selectCell()` and `getCellMeta()`. ::: only-for javascript ```js mergeCells: [ { row: 1, col: 1, rowspan: 3, colspan: 3 }, { row: 3, col: 4, rowspan: 2, colspan: 2 }, { row: 5, col: 6, rowspan: 3, colspan: 3 }, ] ``` ::: ::: only-for react ```jsx mergeCells={[ { row: 1, col: 1, rowspan: 3, colspan: 3 }, { row: 3, col: 4, rowspan: 2, colspan: 2 }, { row: 5, col: 6, rowspan: 3, colspan: 3 }, ]} ``` ::: ::: only-for angular ```ts settings = { mergeCells: [ { row: 1, col: 1, rowspan: 3, colspan: 3 }, { row: 3, col: 4, rowspan: 2, colspan: 2 }, { row: 5, col: 6, rowspan: 3, colspan: 3 }, ], }; ``` ::: ::: only-for vue ```js const hotSettings = { mergeCells: [ { row: 1, col: 1, rowspan: 3, colspan: 3 }, { row: 3, col: 4, rowspan: 2, colspan: 2 }, { row: 5, col: 6, rowspan: 3, colspan: 3 }, ], }; ``` ::: ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/cell-features/merge-cells/javascript/example1.js) @[code](@/content/guides/cell-features/merge-cells/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/merge-cells/react/example1.jsx) @[code](@/content/guides/cell-features/merge-cells/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/merge-cells/angular/example1.ts) @[code](@/content/guides/cell-features/merge-cells/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/cell-features/merge-cells/vue/example1.vue) ::: ::: ## Optimizing rendering of the wide/tall merged cells When cells span thousands of rows or columns, scrolling may feel slower compared to unmerged cells. To improve performance, consider enabling the dedicated virtualization feature for merged cells, which is disabled by default. To enable the merged cells virtualization mode, enable the `virtualized` option: ::: only-for javascript ```js mergeCells: { virtualized: true, cells: [{ row: 1, col: 1, rowspan: 200, colspan: 2 }] } ``` ::: ::: only-for react ```jsx mergeCells={{ virtualized: true, cells: [{ row: 1, col: 1, rowspan: 200, colspan: 2 }] }} ``` ::: ::: only-for angular ```ts settings = { mergeCells: { virtualized: true, cells: [{ row: 1, col: 1, rowspan: 200, colspan: 2 }], }, }; ``` ::: ::: only-for vue ```js const hotSettings = { mergeCells: { virtualized: true, cells: [{ row: 1, col: 1, rowspan: 200, colspan: 2 }], }, }; ``` ::: The example below uses virtualized merged cells. It's also recommended to increase the buffer of rendered rows/columns to minimize the flickering effects. ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/cell-features/merge-cells/javascript/example2.js) @[code](@/content/guides/cell-features/merge-cells/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/merge-cells/react/example2.jsx) @[code](@/content/guides/cell-features/merge-cells/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/merge-cells/angular/example2.ts) @[code](@/content/guides/cell-features/merge-cells/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/cell-features/merge-cells/vue/example2.vue) ::: ::: ## React to merge and unmerge events To run your own logic when cells merge or unmerge, use the [`beforeMergeCells`](@/api/hooks.md#beforemergecells), [`afterMergeCells`](@/api/hooks.md#aftermergecells), [`beforeUnmergeCells`](@/api/hooks.md#beforeunmergecells), and [`afterUnmergeCells`](@/api/hooks.md#afterunmergecells) hooks. Each hook receives the affected `cellRange`, and `afterMergeCells` also receives the resulting `mergeParent` object (`row`, `col`, `rowspan`, `colspan`). The example below logs a message every time you merge or unmerge cells, using the context menu or the **Ctrl**+**M** shortcut. ::: only-for javascript ::: example #example3 --html 1 --js 2 --ts 3 @[code](@/content/guides/cell-features/merge-cells/javascript/example3.html) @[code](@/content/guides/cell-features/merge-cells/javascript/example3.js) @[code](@/content/guides/cell-features/merge-cells/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/merge-cells/react/example3.jsx) @[code](@/content/guides/cell-features/merge-cells/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/merge-cells/angular/example3.ts) @[code](@/content/guides/cell-features/merge-cells/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/cell-features/merge-cells/vue/example3.vue) ::: ::: ## Merge and unmerge cells programmatically To merge or unmerge a range without a user action, call the [`MergeCells`](@/api/mergeCells.md) plugin's `merge()` and `unmerge()` methods. Both methods take a visual `startRow`, `startColumn`, `endRow`, and `endColumn`, the same coordinate space as `selectCell()`. ```js hot.getPlugin('mergeCells').merge(startRow, startColumn, endRow, endColumn); hot.getPlugin('mergeCells').unmerge(startRow, startColumn, endRow, endColumn); ``` This clears the underlying data the same way as merging through the UI or the [`mergeCells`](@/api/options.md#mergecells) configuration option. See [Effect on the underlying data](#effect-on-the-underlying-data). The example below merges and unmerges a footnote row that spans every column, using buttons instead of a manual selection. ::: only-for javascript ::: example #example4 --html 1 --js 2 --ts 3 @[code](@/content/guides/cell-features/merge-cells/javascript/example4.html) @[code](@/content/guides/cell-features/merge-cells/javascript/example4.js) @[code](@/content/guides/cell-features/merge-cells/javascript/example4.ts) ::: ::: ::: only-for react ::: example #example4 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/merge-cells/react/example4.jsx) @[code](@/content/guides/cell-features/merge-cells/react/example4.tsx) ::: ::: ::: only-for angular ::: example #example4 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/merge-cells/angular/example4.ts) @[code](@/content/guides/cell-features/merge-cells/angular/example4.html) ::: ::: ::: only-for vue ::: example #example4 :vue3 @[code](@/content/guides/cell-features/merge-cells/vue/example4.vue) ::: ::: ## Effect on the underlying data Merging doesn't only hide the covered cells visually -- it clears their values in the underlying data too, whether you merge through the UI, the [`mergeCells`](@/api/options.md#mergecells) configuration option, or the `merge()` method. Only the top-left cell of the range keeps its value; every other cell covered by the merge is set to `null`. For a range merged at `(0, 0)` with `rowspan: 2` and `colspan: 2`: ```js hot.getData(); // -> [['Top-left value', null], [null, null]] hot.getSourceData(); // -> [['Top-left value', null], [null, null]] (the same) ``` Both [`getData()`](@/api/core.md#getdata) and [`getSourceData()`](@/api/core.md#getsourcedata) return `null` for the covered cells, because the clearing runs through the normal change pipeline: it fires [`beforeChange`](@/api/hooks.md#beforechange) and [`afterChange`](@/api/hooks.md#afterchange) with `source === 'MergeCells'` (see [Events and hooks: Definition for `source` argument](@/guides/getting-started/events-and-hooks/events-and-hooks.md#definition-for-source-argument)). Unmerging does not restore the cleared values. If you need the original values back, keep a copy before merging, or restore them yourself in a [`beforeUnmergeCells`](@/api/hooks.md#beforeunmergecells) or [`afterUnmergeCells`](@/api/hooks.md#afterunmergecells) handler. ## Effect on viewport getter methods With merged cells, the rendered range extends to fit any merged cell that crosses the viewport edge. This is the same expansion that the `virtualized` option turns off. As a result, the rendered-range getters can return indexes beyond what you see on the screen: - [`getFirstRenderedVisibleRow()`](@/api/core.md#getfirstrenderedvisiblerow), [`getLastRenderedVisibleRow()`](@/api/core.md#getlastrenderedvisiblerow), [`getFirstRenderedVisibleColumn()`](@/api/core.md#getfirstrenderedvisiblecolumn), and [`getLastRenderedVisibleColumn()`](@/api/core.md#getlastrenderedvisiblecolumn). - [`AutoRowSize.getFirstVisibleRow()`](@/api/autoRowSize.md#getfirstvisiblerow) and [`AutoRowSize.getLastVisibleRow()`](@/api/autoRowSize.md#getlastvisiblerow), which delegate to the rendered-row getters. - [`AutoColumnSize.getFirstVisibleColumn()`](@/api/autoColumnSize.md#getfirstvisiblecolumn) and [`AutoColumnSize.getLastVisibleColumn()`](@/api/autoColumnSize.md#getlastvisiblecolumn), which delegate to the rendered-column getters. For example, a merged cell that spans columns 0 to 100 makes `getLastVisibleColumn()` return an index near 100, even when the viewport shows far fewer columns. To read the actual visible viewport, use the fully-visible or partially-visible getters, which ignore the merge-cell expansion: - [`getFirstFullyVisibleRow()`](@/api/core.md#getfirstfullyvisiblerow), [`getLastFullyVisibleRow()`](@/api/core.md#getlastfullyvisiblerow), [`getFirstFullyVisibleColumn()`](@/api/core.md#getfirstfullyvisiblecolumn), and [`getLastFullyVisibleColumn()`](@/api/core.md#getlastfullyvisiblecolumn). - [`getFirstPartiallyVisibleRow()`](@/api/core.md#getfirstpartiallyvisiblerow), [`getLastPartiallyVisibleRow()`](@/api/core.md#getlastpartiallyvisiblerow), [`getFirstPartiallyVisibleColumn()`](@/api/core.md#getfirstpartiallyvisiblecolumn), and [`getLastPartiallyVisibleColumn()`](@/api/core.md#getlastpartiallyvisiblecolumn). Setting `virtualized` to `true` also removes the range expansion, but it is a performance option -- the rendered-range getters still include buffered rows and columns outside the viewport. ## Behavior during row/column reorder and column freeze When a merged cell's underlying rows or columns are reordered (through [`manualColumnMove`](@/api/options.md#manualcolumnmove), [`manualRowMove`](@/api/options.md#manualrowmove), or [`manualColumnFreeze`](@/api/options.md#manualcolumnfreeze)), Handsontable follows the merge to the new visual position. Two side effects can occur: - **Auto-split**: if the move bisects a merge so the underlying cells are no longer contiguous in the new visual order, the merge is split into separate merges, one per contiguous run. The cross-axis span (`rowspan` for column moves, `colspan` for row moves) is preserved on every fragment. - **Silent drop of single-cell fragments**: any resulting fragment that ends up as a single cell (`rowspan === 1 && colspan === 1`) is removed, because a single cell is no longer a merge. The [`afterMergeCells`](@/api/hooks.md#aftermergecells) hook is not fired for the dropped fragment. [`undo`](@/api/options.md#undo) and [`redo`](@/api/options.md#redo) restore the pre-move state, including any merges that were split or dropped by the reorder. ## Result Cells at the configured positions are now merged. Users see a single cell spanning multiple rows or columns. ## Related keyboard shortcuts | Windows | macOS | Action | Excel | Sheets | | -------------------------------------- | -------------------------------------- | ----------------------------------- | :-----: | :-----: | | **Ctrl**+**M** | ⌃+**M** | Merge or unmerge the selected cells | ✗ | ✗ | ## Related API reference **Configuration options**
- [mergeCells](@/api/options.md#mergecells) - [viewportColumnRenderingThreshold](@/api/options.md#viewportcolumnrenderingthreshold) - [viewportRowRenderingThreshold](@/api/options.md#viewportrowrenderingthreshold) - [viewportColumnRenderingOffset](@/api/options.md#viewportcolumnrenderingoffset) - [viewportRowRenderingOffset](@/api/options.md#viewportrowrenderingoffset)
**Hooks**
- [afterMergeCells](@/api/hooks.md#aftermergecells) - [afterUnmergeCells](@/api/hooks.md#afterunmergecells) - [beforeMergeCells](@/api/hooks.md#beforemergecells) - [beforeUnmergeCells](@/api/hooks.md#beforeunmergecells)
**Plugins**
- [MergeCells](@/api/mergeCells.md)
--- # Conditional formatting URL: https://handsontable.com/docs/javascript-data-grid/conditional-formatting/ Style cells dynamically based on their values, so you can highlight outliers, flag errors, or visualize data ranges. [[toc]] ## Overview Conditional formatting changes a cell's appearance based on the data it holds. As values change, the formatting updates on the next render. Choose an approach based on your goal: - Use [`className`](@/api/options.md#classname) with the [`cells`](@/api/options.md#cells) callback when you want reusable CSS classes driven by cell values. This is the recommended default. - Use the [`renderer`](@/api/options.md#renderer) option when you need inline styles or want to transform the displayed value. - Combine a `renderer` with a computed background color when you want a color scale that reflects each value's magnitude. For static styling that does not depend on cell values, see [Formatting cells](@/guides/cell-features/formatting-cells/formatting-cells.md). ## Prerequisites - A Handsontable instance with data loaded. - A stylesheet, if you format cells through custom CSS classes. - Familiarity with the cascading configuration model, where cell settings override column settings, which override global settings. See [Configuration options](@/guides/getting-started/configuration-options/configuration-options.md). ## Highlight cells by value with a CSS class Return a [`className`](@/api/options.md#classname) from the [`cells`](@/api/options.md#cells) callback to style cells by value. The callback runs for every cell, so you can inspect the value and assign a class conditionally. This example uses the cascading model on two levels: the Company column sets a static `className` through its `columns` entry, while the [`cells`](@/api/options.md#cells) callback adds a `loss` class to negative quarters and a `strong-quarter` class to quarters above 10. The `loss` class pairs its red color with a leading marker, so the value stays readable without color. ::: only-for javascript ::: example #example1 --css 1 --js 2 --ts 3 @[code](@/content/guides/cell-features/conditional-formatting/javascript/example1.css) @[code](@/content/guides/cell-features/conditional-formatting/javascript/example1.js) @[code](@/content/guides/cell-features/conditional-formatting/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --css 1 --js 2 --ts 3 @[code](@/content/guides/cell-features/conditional-formatting/react/example1.css) @[code](@/content/guides/cell-features/conditional-formatting/react/example1.jsx) @[code](@/content/guides/cell-features/conditional-formatting/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/conditional-formatting/angular/example1.ts) @[code](@/content/guides/cell-features/conditional-formatting/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/cell-features/conditional-formatting/vue/example1.vue) ::: ::: ## Format cells with a custom renderer Use a [`renderer`](@/api/options.md#renderer) when a CSS class is not enough - for example, when you need to transform the displayed value or apply inline styles computed at render time. This renderer formats each quarter as a currency amount and shows losses in an accounting format, `($1.3M)`. The parentheses convey a loss on their own, so the red color is a secondary signal. ::: only-for javascript ::: example #example2 --css 1 --js 2 --ts 3 @[code](@/content/guides/cell-features/conditional-formatting/javascript/example2.css) @[code](@/content/guides/cell-features/conditional-formatting/javascript/example2.js) @[code](@/content/guides/cell-features/conditional-formatting/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --css 1 --js 2 --ts 3 @[code](@/content/guides/cell-features/conditional-formatting/react/example2.css) @[code](@/content/guides/cell-features/conditional-formatting/react/example2.jsx) @[code](@/content/guides/cell-features/conditional-formatting/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/conditional-formatting/angular/example2.ts) @[code](@/content/guides/cell-features/conditional-formatting/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/cell-features/conditional-formatting/vue/example2.vue) ::: ::: ## Build a color scale To visualize magnitude, compute a background color from each value and apply it in a [`renderer`](@/api/options.md#renderer). This example maps every quarter to a green-to-red scale based on the minimum and maximum across the data, producing a heatmap. The numeric value remains visible in each cell, so the color adds meaning rather than replacing it. ::: only-for javascript ::: example #example3 --js 1 --ts 2 @[code](@/content/guides/cell-features/conditional-formatting/javascript/example3.js) @[code](@/content/guides/cell-features/conditional-formatting/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/conditional-formatting/react/example3.jsx) @[code](@/content/guides/cell-features/conditional-formatting/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/conditional-formatting/angular/example3.ts) @[code](@/content/guides/cell-features/conditional-formatting/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/cell-features/conditional-formatting/vue/example3.vue) ::: ::: ## Make conditional formatting accessible Color alone does not satisfy [WCAG 2.2 Success Criterion 1.4.1 Use of Color](https://www.w3.org/WAI/WCAG22/quickref/#use-of-color), a Level A criterion. Readers with color vision deficiencies, or anyone viewing the grid in high-contrast mode, can miss a signal that relies only on color. - Pair color with a second cue: a symbol, text label, or font weight. In the first example, the `loss` class adds a marker; in the second, losses appear in parentheses. - In a color scale, keep the underlying value visible so the color stays supplementary. - Verify that text keeps a contrast ratio of at least 4.5:1 against the cell background you apply. ## Result Cells that match your conditions display the configured styles - classes, inline styles, or a color scale - and update automatically as the data changes. ## Related articles **Related guides**
- [Formatting cells](@/guides/cell-features/formatting-cells/formatting-cells.md) - [Text alignment](@/guides/cell-features/text-alignment/text-alignment.md) - [Configuration options](@/guides/getting-started/configuration-options/configuration-options.md)
**Configuration options**
- [className](@/api/options.md#classname) - [cells](@/api/options.md#cells) - [renderer](@/api/options.md#renderer)
--- # Text alignment URL: https://handsontable.com/docs/javascript-data-grid/text-alignment/ Align values within cells: horizontally (to the right, left, center, or by justifying them), and vertically (to the top, middle, or bottom of the cell). [[toc]] Apply text alignment to cells using CSS class names or the `className` configuration option. ## Overview You can set cell alignment in two ways: | Configuration option
`className`/`cells` | Context menu
`alignment` item | |---|---| | Set programmatically, for the whole grid, a column, or individual cells | Set interactively, by the person using the grid | | Requires no additional plugin | Requires the [`ContextMenu`](@/guides/accessories-and-menus/context-menu/context-menu.md) plugin, with the `alignment` item enabled | | Not tracked by [`UndoRedo`](@/guides/accessories-and-menus/undo-redo/undo-redo.md) | Tracked by [`UndoRedo`](@/guides/accessories-and-menus/undo-redo/undo-redo.md) | ## To align a cell To set alignment for individual cells, configure them using the [`cells`](@/api/options.md#cells) option or the [`cell`](@/api/options.md#cell) array. Available class names: - Horizontal: `htLeft`, `htCenter`, `htRight`, `htJustify` - Vertical: `htTop`, `htMiddle`, `htBottom` You can track alignment changes by using the [`afterSetCellMeta`](@/api/hooks.md#aftersetcellmeta) hook. ## To align a column To apply alignment globally or per column, provide the alignment details in the [`className`](@/api/options.md#classname) option, for example: ::: only-for javascript ```js className: 'htCenter' ``` ::: ::: only-for react ```jsx className="htCenter" ``` ::: ::: only-for angular ```ts settings = { className: "htCenter" }; ``` ::: ::: only-for vue ```js const hotSettings = { className: 'htCenter', }; ``` ::: ## Align cells using the context menu Let the person using the grid set alignment interactively, through the [context menu](@/guides/accessories-and-menus/context-menu/context-menu.md). Enable the `ContextMenu` plugin and include the `alignment` item: ```js contextMenu: ['alignment'], ``` Select one or more cells, right-click to open the context menu, and choose an option from the **Align** submenu: - Horizontal: **Left**, **Center**, **Right**, **Justify** - Vertical: **Top**, **Middle**, **Bottom** Each option sets the matching CSS class name (`htLeft`, `htCenter`, `htRight`, `htJustify`, `htTop`, `htMiddle`, `htBottom`) on the selected cells, and fires the [`beforeCellAlignment`](@/api/hooks.md#beforecellalignment) hook before applying the change. ### Undo and redo alignment changes Alignment changes made through the context menu are tracked by the [`UndoRedo`](@/guides/accessories-and-menus/undo-redo/undo-redo.md) plugin. Press **Ctrl**/⌘+**Z** to undo an alignment change, and **Ctrl**/⌘+**Y** to redo it. Alignment changes made through the `className` or `cells` configuration options aren't tracked by `UndoRedo`. ## Basic example The following code sample configures the grid to use `htCenter` and configures individual cells to use different alignments. ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/cell-features/text-alignment/javascript/example1.js) @[code](@/content/guides/cell-features/text-alignment/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/text-alignment/react/example1.jsx) @[code](@/content/guides/cell-features/text-alignment/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/text-alignment/angular/example1.ts) @[code](@/content/guides/cell-features/text-alignment/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/cell-features/text-alignment/vue/example1.vue) ::: ::: ## Result Cells display the configured horizontal or vertical alignment. Global settings apply to all cells, and per-cell settings take precedence over the global defaults. ## Related API reference **Configuration options**
- [className](@/api/options.md#classname) - [contextMenu](@/api/options.md#contextmenu)
**Hooks**
- [afterSetCellMeta](@/api/hooks.md#aftersetcellmeta) - [beforeCellAlignment](@/api/hooks.md#beforecellalignment)
**Plugins**
- [ContextMenu](@/api/contextMenu.md) - [UndoRedo](@/api/undoRedo.md)
--- # Read-only cells URL: https://handsontable.com/docs/javascript-data-grid/read-only-cells/ Make specified cells read-only to protect them from unwanted changes but still allow navigation and copying of data. [[toc]] Disable individual cells, entire columns, or entire rows to prevent user edits. Use `readOnly` on cells, columns, or the whole grid. ## Overview Disabling a cell makes the cell read-only or non-editable. Both have similar outcomes, with the following differences: | Read-only cell
`readOnly: true` | Non-editable cell
`editor: false` | |------------------------------------------------------------------------------| -------------------------------------------------------------------------- | | Has an additional CSS class (`htDimmed`) | Has no additional CSS class | | Copy works, paste doesn't work | Copy-paste works | | Drag-to-fill doesn't work | Drag-to-fill works | | Can't be changed by [`populateFromArray()`](@/api/core.md#populatefromarray) | Can be changed by [`populateFromArray()`](@/api/core.md#populatefromarray) | ## Make the grid read-only To make the entire grid read-only, set [`readOnly`](@/api/options.md#readonly) to `true` as a [top-level grid option](@/guides/getting-started/configuration-options/configuration-options.md#set-grid-options). ::: only-for javascript ::: example #exampleReadOnlyGrid --js 1 --ts 2 @[code](@/content/guides/cell-features/read-only-cells/javascript/exampleReadOnlyGrid.js) @[code](@/content/guides/cell-features/read-only-cells/javascript/exampleReadOnlyGrid.ts) ::: ::: ::: only-for react ::: example #exampleReadOnlyGrid :react --js 1 --ts 2 @[code](@/content/guides/cell-features/read-only-cells/react/exampleReadOnlyGrid.jsx) @[code](@/content/guides/cell-features/read-only-cells/react/exampleReadOnlyGrid.tsx) ::: ::: ::: only-for angular ::: example #exampleReadOnlyGrid :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/read-only-cells/angular/exampleReadOnlyGrid.ts) @[code](@/content/guides/cell-features/read-only-cells/angular/exampleReadOnlyGrid.html) ::: ::: ::: only-for vue ::: example #exampleReadOnlyGrid :vue3 @[code](@/content/guides/cell-features/read-only-cells/vue/exampleReadOnlyGrid.vue) ::: ::: ## Make a column read-only To make a column read-only, declare it in the [`columns`](@/api/options.md#columns) configuration option. The column remains available for keyboard navigation and copying data (**Ctrl**/**Cmd**+**C**), but editing and pasting are disabled. The example below also defines a custom renderer that dims the read-only column, giving the user a visual cue that its cells are read-only. ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/cell-features/read-only-cells/javascript/example1.js) @[code](@/content/guides/cell-features/read-only-cells/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/read-only-cells/react/example1.jsx) @[code](@/content/guides/cell-features/read-only-cells/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/read-only-cells/angular/example1.ts) @[code](@/content/guides/cell-features/read-only-cells/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/cell-features/read-only-cells/vue/example1.vue) ::: ::: ## Make a row read-only To make an entire row read-only, use the [`cells`](@/api/options.md#cells) function and set the [`readOnly`](@/api/options.md#readonly) property based on the row index, regardless of the column. The example below makes the second row (index `1`) read-only. ::: only-for javascript ::: example #example5 --js 1 --ts 2 @[code](@/content/guides/cell-features/read-only-cells/javascript/example5.js) @[code](@/content/guides/cell-features/read-only-cells/javascript/example5.ts) ::: ::: ::: only-for react ::: example #example5 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/read-only-cells/react/example5.jsx) @[code](@/content/guides/cell-features/read-only-cells/react/example5.tsx) ::: ::: ::: only-for angular ::: example #example5 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/read-only-cells/angular/example5.ts) @[code](@/content/guides/cell-features/read-only-cells/angular/example5.html) ::: ::: ::: only-for vue ::: example #example5 :vue3 @[code](@/content/guides/cell-features/read-only-cells/vue/example5.vue) ::: ::: ## Make specific cells read-only To make specific cells read-only, use the [`cells`](@/api/options.md#cells) function to set the [`readOnly`](@/api/options.md#readonly) property conditionally. The example below makes cells that contain the word "Nissan" read-only. ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/cell-features/read-only-cells/javascript/example2.js) @[code](@/content/guides/cell-features/read-only-cells/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/read-only-cells/react/example2.jsx) @[code](@/content/guides/cell-features/read-only-cells/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/read-only-cells/angular/example2.ts) @[code](@/content/guides/cell-features/read-only-cells/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/cell-features/read-only-cells/vue/example2.vue) ::: ::: Non-editable cells behave like any other cells apart from preventing you from manually changing their values. ## Make a column non-editable To make a column non-editable, declare it in the [`columns`](@/api/options.md#columns) configuration option. The column's basic behavior does not change -- you can still use keyboard navigation, **Ctrl**/**Cmd**+**C**, **Ctrl**/**Cmd**+**V**, and drag-to-fill. The example below also defines a custom renderer that dims the non-editable column, giving the user a visual cue that its cells are non-editable. ::: only-for javascript ::: example #example3 --js 1 --ts 2 @[code](@/content/guides/cell-features/read-only-cells/javascript/example3.js) @[code](@/content/guides/cell-features/read-only-cells/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/read-only-cells/react/example3.jsx) @[code](@/content/guides/cell-features/read-only-cells/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/read-only-cells/angular/example3.ts) @[code](@/content/guides/cell-features/read-only-cells/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/cell-features/read-only-cells/vue/example3.vue) ::: ::: ## Make specific cells non-editable To make specific cells non-editable, set `editor: false` in the cell configuration. The following example shows a table with non-editable cells containing the word "Nissan". ::: only-for javascript ::: example #example4 --js 1 --ts 2 @[code](@/content/guides/cell-features/read-only-cells/javascript/example4.js) @[code](@/content/guides/cell-features/read-only-cells/javascript/example4.ts) ::: ::: ::: only-for react ::: example #example4 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/read-only-cells/react/example4.jsx) @[code](@/content/guides/cell-features/read-only-cells/react/example4.tsx) ::: ::: ::: only-for angular ::: example #example4 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/read-only-cells/angular/example4.ts) @[code](@/content/guides/cell-features/read-only-cells/angular/example4.html) ::: ::: ::: only-for vue ::: example #example4 :vue3 @[code](@/content/guides/cell-features/read-only-cells/vue/example4.vue) ::: ::: ## Block the entire grid The [`readOnly`](@/api/options.md#readonly) and [`editor`](@/api/options.md#editor) options control editing, but users can still navigate and select cells. To block all interaction with the grid temporarily -- for example, during a loading state or a blocking workflow step -- use the [Dialog](@/guides/dialog/dialog/dialog.md) plugin. An open dialog overlays the grid and moves keyboard input into its own [focus scope](@/guides/navigation/focus-scopes/focus-scopes.md), so users can't reach the cells until the dialog closes. The grid regains focus automatically. ## Accessibility When [`ariaTags`](@/api/options.md#ariatags) is enabled (the default), Handsontable adds `aria-readonly="true"` to the DOM element of every read-only cell, so screen readers announce that the cell can't be edited. Non-editable cells (`editor: false`) don't get this attribute, because Handsontable doesn't treat them as read-only in the data model -- only their editor is disabled. For more accessibility features and testing guidance, see [Accessibility](@/guides/accessibility/accessibility/accessibility.md). ## Result Read-only cells display with the `htDimmed` CSS class and block paste and drag-to-fill operations. Non-editable cells block manual editing but allow copy-paste and drag-to-fill. ## Related API reference **Configuration options**
- [ariaTags](@/api/options.md#ariatags) - [readOnly](@/api/options.md#readonly) - [readOnlyCellClassName](@/api/options.md#readonlycellclassname)
--- # Comments URL: https://handsontable.com/docs/javascript-data-grid/comments/ Add a comment (a note) to a cell, using the context menu, just like in Excel. Edit and delete comments. Make comments read-only. [[toc]] The Comments plugin lets users attach text notes to individual cells. Use it when reviewers need to annotate data without changing cell values. The plugin shows and edits a comment in a text field, so it doesn't parse the comment as HTML. If you use the comment text elsewhere, for example in your own interface, escape or sanitize it first. ## Enable the plugin Set the [`comments`](@/api/options.md#comments) configuration option to `true` to enable the feature and add all the needed context menu items. For example: ::: only-for javascript ```js const hot = new Handsontable(container, { data: [ ['Update API docs', 'Ana García', 'In progress'], ['Deploy hotfix', 'James Okafor', 'Blocked'], ], comments: true, autoWrapRow: true, autoWrapCol: true }); ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts data = [ ["Update API docs", "Ana García", "In progress"], ["Deploy hotfix", "James Okafor", "Blocked"], ]; settings = { comments: true, }; ``` ```html ``` ::: ::: only-for vue ```js const hotSettings = { data: [ ['Update API docs', 'Ana García', 'In progress'], ['Deploy hotfix', 'James Okafor', 'Blocked'], ], comments: true, autoWrapRow: true, autoWrapCol: true, licenseKey: 'non-commercial-and-evaluation', }; ``` ::: ## Add comments via the context menu After you've enabled the plugin, the [Context Menu](@/guides/accessories-and-menus/context-menu/context-menu.md) gains a few new items: - Add/Edit comment - Delete comment - Read-only comment ## Set up pre-set comments You can also pre-define comments for your table. Comments are stored in the table's/column's/cell's metadata object and you can declare as any value of the respective type. For example: ::: only-for javascript ```js cell: [ { row: 1, col: 1, comment: { value: 'Hello world!' } } ] ``` ::: ::: only-for react ```jsx cell={[ { row: 1, col: 1, comment: { value: 'Hello world!' } } ]} ``` ::: ::: only-for angular ```ts settings = { cell: [{ row: 1, col: 1, comment: { value: "Hello world!" } }], }; ``` ::: ::: only-for vue ```js const hotSettings = { cell: [ { row: 1, col: 1, comment: { value: 'Hello world!' } }, ], }; ``` ::: In this example, the comment "Hello world!" is added to the cell at `(1,1)`. ## Basic example ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/cell-features/comments/javascript/example1.js) @[code](@/content/guides/cell-features/comments/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/comments/react/example1.jsx) @[code](@/content/guides/cell-features/comments/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/comments/angular/example1.ts) @[code](@/content/guides/cell-features/comments/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/cell-features/comments/vue/example1.vue) ::: ::: ## Make a comment read-only By default, all comments are editable. To change this, set the [`readOnly`](@/api/options.md#readonly) configuration option to `true` when adding a comment. This example makes the "Tesla" comment attached to a cell read-only, whereas the "Honda" comment attached to another cell is editable. ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/cell-features/comments/javascript/example2.js) @[code](@/content/guides/cell-features/comments/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/comments/react/example2.jsx) @[code](@/content/guides/cell-features/comments/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/comments/angular/example2.ts) @[code](@/content/guides/cell-features/comments/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/cell-features/comments/vue/example2.vue) ::: ::: ## Set a comment box's size To set the width and height of a comment box, use the [`style`](@/api/options.md#comments) parameter. ::: only-for javascript ::: example #example3 --js 1 --ts 2 @[code](@/content/guides/cell-features/comments/javascript/example3.js) @[code](@/content/guides/cell-features/comments/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/comments/react/example3.jsx) @[code](@/content/guides/cell-features/comments/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/comments/angular/example3.ts) @[code](@/content/guides/cell-features/comments/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/cell-features/comments/vue/example3.vue) ::: ::: ## Set a delay for displaying comments To display comments after a pre-configured time delay, use the [`displayDelay`](@/api/options.md#comments) parameter. ::: only-for javascript ::: example #example4 --js 1 --ts 2 @[code](@/content/guides/cell-features/comments/javascript/example4.js) @[code](@/content/guides/cell-features/comments/javascript/example4.ts) ::: ::: ::: only-for react ::: example #example4 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/comments/react/example4.jsx) @[code](@/content/guides/cell-features/comments/react/example4.tsx) ::: ::: ::: only-for angular ::: example #example4 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/comments/angular/example4.ts) @[code](@/content/guides/cell-features/comments/angular/example4.html) ::: ::: ::: only-for vue ::: example #example4 :vue3 @[code](@/content/guides/cell-features/comments/vue/example4.vue) ::: ::: ## Flag invalid cells with a comment Combine comments with validation to explain data-entry errors. This example adds a comment to a cell when it fails validation and removes the comment once the value is valid. The [`afterValidate`](@/api/hooks.md#aftervalidate) hook drives the change, and [`setCommentAtCell()`](@/api/comments.md#setcommentatcell) writes the message. The grid validates on load, so the **Mechanical keyboard** row starts with an invalid stock value and shows its comment right away. Edit any **Stock** cell and enter a negative number or text to flag it, or enter a valid whole number to clear the flag. ::: only-for javascript ::: example #example5 --js 1 --ts 2 @[code](@/content/guides/cell-features/comments/javascript/example5.js) @[code](@/content/guides/cell-features/comments/javascript/example5.ts) ::: ::: ::: only-for react ::: example #example5 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/comments/react/example5.jsx) @[code](@/content/guides/cell-features/comments/react/example5.tsx) ::: ::: ::: only-for angular ::: example #example5 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/comments/angular/example5.ts) @[code](@/content/guides/cell-features/comments/angular/example5.html) ::: ::: ::: only-for vue ::: example #example5 :vue3 @[code](@/content/guides/cell-features/comments/vue/example5.vue) ::: ::: ## Read all comments programmatically To collect every comment in the grid, loop through the rows and read each cell's metadata with [`getCellMetaAtRow()`](@/api/core.md#getcellmetaatrow). Each returned cell-meta object stores its comment under the `comment` key. Click **List all comments** to print all comments below the grid. ::: only-for javascript ::: example #example6 --html 1 --js 2 --ts 3 @[code](@/content/guides/cell-features/comments/javascript/example6.html) @[code](@/content/guides/cell-features/comments/javascript/example6.js) @[code](@/content/guides/cell-features/comments/javascript/example6.ts) ::: ::: ::: only-for react ::: example #example6 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/comments/react/example6.jsx) @[code](@/content/guides/cell-features/comments/react/example6.tsx) ::: ::: ::: only-for angular ::: example #example6 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/comments/angular/example6.ts) @[code](@/content/guides/cell-features/comments/angular/example6.html) ::: ::: ::: only-for vue ::: example #example6 :vue3 @[code](@/content/guides/cell-features/comments/vue/example6.vue) ::: ::: ## Result Cells with comments display a small indicator in the corner. Users can view, edit, or delete comments through the context menu, and pre-configured comments appear when the table loads. ## Related keyboard shortcuts | Windows | macOS | Action | Excel | Sheets | | ------------------------------------------------------- | ---------------------------------------------------------- | --------------------------------------- | :-----: | :-----: | | **Ctrl**+**Alt**+**M** | ⌃+⌥+**M** | Add or edit a comment | ✗ | ✓ | | **Ctrl**+**Enter** | ⌘+**Enter** | Save and exit the current comment | ✗ | ✓ | | **Escape** | **Escape** | Exit the current comment without saving | ✗ | ✗ | ## Related API reference **Configuration options**
- [commentedCellClassName](@/api/options.md#commentedcellclassname) - [comments](@/api/options.md#comments)
**Hooks**
- [afterRemoveCellMeta](@/api/hooks.md#afterremovecellmeta) - [afterSetCellMeta](@/api/hooks.md#aftersetcellmeta) - [beforeRemoveCellMeta](@/api/hooks.md#beforeremovecellmeta) - [beforeSetCellMeta](@/api/hooks.md#beforesetcellmeta)
**Plugins**
- [Comments](@/api/comments.md)
--- # Autofill values URL: https://handsontable.com/docs/javascript-data-grid/autofill-values/ Copy a cell's value into multiple other cells, using the "fill handle" UI element. Configure the direction of copying, and more, through Handsontable's API. [[toc]] Autofill lets users drag the fill handle to copy or extend values across adjacent cells. Use it to speed up repetitive data entry. ## Autofill in all directions Using the tiny square known as the 'fill handle' in the corner of the selected cell, you can drag it to repeat or extend values across adjacent cells. You can also **double-click the fill handle** to autofill downward without dragging. Select the four cells from Symbol to Known Isotopes in the `Lithium` row (row header 3) and double-click the fill handle to see this in action. ### How double-click autofill determines the range Handsontable scans the rows below your selection and fills down to the last row where the column immediately to the left or right of your selection contains a value. In the example below, the Name column acts as the guide -- the rows with row headers 4 and 5 have values there, so the fill extends through both rows. Two conditions must be met for the fill to happen: - **All cells below the selection in the filled column(s) must be empty.** If any cell below the selection in those columns contains data, double-clicking does nothing. - At least one column adjacent to your selection must have data in the rows below. **Visual difference from drag-fill:** When you drag the fill handle, a preview border shows the target range as you drag. When you double-click, no drag-preview appears -- the cells populate immediately based on the adjacent column data. ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/cell-features/autofill-values/javascript/example1.js) @[code](@/content/guides/cell-features/autofill-values/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/autofill-values/react/example1.jsx) @[code](@/content/guides/cell-features/autofill-values/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/autofill-values/angular/example1.ts) @[code](@/content/guides/cell-features/autofill-values/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/cell-features/autofill-values/vue/example1.vue) ::: ::: ## Autofill in a vertical direction only and creating new rows In this configuration, the fill handle is restricted to move only vertically. New rows are automatically added to the bottom of the table by changing [`autoInsertRow`](@/api/options.md#fillhandle) to `true`. ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/cell-features/autofill-values/javascript/example2.js) @[code](@/content/guides/cell-features/autofill-values/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/autofill-values/react/example2.jsx) @[code](@/content/guides/cell-features/autofill-values/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/autofill-values/angular/example2.ts) @[code](@/content/guides/cell-features/autofill-values/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/cell-features/autofill-values/vue/example2.vue) ::: ::: ## Altering and tracking autofilled values Use the [`beforeAutofill`](@/api/hooks.md#beforeautofill) hook to change the values Handsontable is about to fill in, and the [`afterAutofill`](@/api/hooks.md#afterautofill) hook to react once the fill completes. In the example below, select the four cells from Symbol to Known Isotopes in the `Lithium` row and drag the fill handle down through the rows with row headers 4 and 5. The `afterAutofill` hook logs the affected range and direction to the output box below the grid. The logged row and column numbers are 0-based indices, so the two filled rows appear as `3-4`, not `4-5`. The `beforeAutofill` hook in this example rounds every filled number up to the nearest multiple of 5. This is a demonstration of what the hook can do, not the default autofill result: without the hook, Handsontable repeats the selected values as they are. When Handsontable fires [`beforeChange`](@/api/hooks.md#beforechange) or [`afterChange`](@/api/hooks.md#afterchange) as part of an autofill operation, their `source` argument is `Autofill.fill`. Read more about the `source` argument in [Events and hooks: Definition for `source` argument](@/guides/getting-started/events-and-hooks/events-and-hooks.md#definition-for-source-argument). ::: only-for javascript ::: example #example3 --html 1 --js 2 --ts 3 @[code](@/content/guides/cell-features/autofill-values/javascript/example3.html) @[code](@/content/guides/cell-features/autofill-values/javascript/example3.js) @[code](@/content/guides/cell-features/autofill-values/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/autofill-values/react/example3.jsx) @[code](@/content/guides/cell-features/autofill-values/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/autofill-values/angular/example3.ts) @[code](@/content/guides/cell-features/autofill-values/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/cell-features/autofill-values/vue/example3.vue) ::: ::: ## Autofill and formulas With the [`Formulas`](@/guides/formulas/formula-calculation/formula-calculation.md) plugin enabled, autofill delegates to the HyperFormula engine instead of copying cell contents literally: - **Relative references adjust per target cell.** Filling `=A1+B1` down from row 1 to row 2 produces `=A2+B2` in the new cell, the same way a spreadsheet application adjusts formulas on fill. - **Absolute references stay fixed.** Filling `=$A$1+B1` down keeps `$A$1` unchanged in every filled cell, while the relative `B1` part still adjusts. - **The fill is cancelled** if the engine reports that the target cells can't be written to (for example, because they're part of another formula's dependency chain), leaving the target range unchanged. ## Result The fill handle appears on the selected cell. Dragging it copies or extends values into adjacent cells in the configured direction. ## Related API reference **Configuration options**
- [fillHandle](@/api/options.md#fillhandle)
**Hooks**
- [afterAutofill](@/api/hooks.md#afterautofill) - [beforeAutofill](@/api/hooks.md#beforeautofill) - [modifyAutofillRange](@/api/hooks.md#modifyautofillrange)
**Plugins**
- [Autofill](@/api/autofill.md)
--- # Formatting cells URL: https://handsontable.com/docs/javascript-data-grid/formatting-cells/ Change the appearance of cells, using custom CSS classes, inline styles, or custom cell borders. [[toc]] ## Overview Handsontable renders an HTML `table`, so you can style existing `tr` and `td` elements or attach your own classes. Choose an approach based on your goal: - Use [`className`](@/api/options.md#classname) when you want reusable static styles. - Use [`renderer`](@/api/options.md#renderer) when you need to apply inline styles to specific cells at render time. - Use [`customBorders`](@/api/options.md#customborders) when you need custom border widths, colors, or styles for selected ranges. ## Prerequisites - A Handsontable instance with data loaded. - A stylesheet, if you format cells through custom CSS classes. ## Apply custom CSS class styles In this example, you add a custom class `custom-cell` to the cell in the top-left corner and a `custom-table` CSS class that highlights the table headers. To add a CSS class to a cell, column, or row, use the [`className`](@/api/options.md#classname) option. Set it in the grid configuration, in a `columns` entry, or per cell through the [`cells`](@/api/options.md#cells) callback. The class you provide is added to the cell's `` element, where your CSS rules can target it. ::: only-for javascript ::: example #example1 --css 1 --js 2 --ts 3 @[code](@/content/guides/cell-features/formatting-cells/javascript/example1.css) @[code](@/content/guides/cell-features/formatting-cells/javascript/example1.js) @[code](@/content/guides/cell-features/formatting-cells/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --css 1 --js 2 --ts 3 @[code](@/content/guides/cell-features/formatting-cells/react/example1.css) @[code](@/content/guides/cell-features/formatting-cells/react/example1.jsx) @[code](@/content/guides/cell-features/formatting-cells/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/formatting-cells/angular/example1.ts) @[code](@/content/guides/cell-features/formatting-cells/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/cell-features/formatting-cells/vue/example1.vue) ::: ::: ### Give your rule enough specificity A theme styles cells through rules such as `.ht-theme-main .htCore td`, which is more specific than a single class selector. A rule like `.custom-cell { font-size: 24px; }` therefore loses to the theme, and the cell keeps the theme's font size. To win, include the theme class and the element in your selector: ```css .ht-theme-main .htCore td.custom-cell { font-size: 24px; } ``` Adding `!important` to your original rule works too, but the selector above keeps the cascade readable. [`autoRowSize`](@/api/options.md#autorowsize) and [`autoColumnSize`](@/api/options.md#autocolumnsize) measure each cell with its class applied, so a class that changes a cell's font size, padding, or borders is reflected in the calculated row heights and column widths. ## Apply inline styles Apply inline styles directly to a cell's DOM element through the `style` property. Use the [`renderer`](@/api/options.md#renderer) option to run this logic on each render. ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/cell-features/formatting-cells/javascript/example2.js) @[code](@/content/guides/cell-features/formatting-cells/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/formatting-cells/react/example2.jsx) @[code](@/content/guides/cell-features/formatting-cells/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/formatting-cells/angular/example2.ts) @[code](@/content/guides/cell-features/formatting-cells/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/cell-features/formatting-cells/vue/example2.vue) ::: ::: ## Custom cell borders To enable custom borders, set [`customBorders`](@/api/options.md#customborders). You can set it to `true` or pass an array with predefined border configuration. For all settings and methods, see the [API reference](@/api/customBorders.md). In API property names, `start` and `end` refer to the starting and ending edges of the [layout direction](@/guides/internationalization/layout-direction/layout-direction.md). You can customize the border style using the `style` property in the border configuration. The available options are: - `'solid'` (default) - A solid line border - `'dashed'` - A dashed line border - `'dotted'` - A dotted line border The `style` property can be set for any border edge (`top`, `bottom`, `start`, `end`). When not specified, it defaults to `'solid'`. The following example shows different border styles on selected cell ranges. ::: only-for javascript ::: example #example3 --js 1 --ts 2 @[code](@/content/guides/cell-features/formatting-cells/javascript/example3.js) @[code](@/content/guides/cell-features/formatting-cells/javascript/example3.ts) ::: ::: ::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/cell-features/formatting-cells/react/example3.jsx) @[code](@/content/guides/cell-features/formatting-cells/react/example3.tsx) ::: ::: ::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/cell-features/formatting-cells/angular/example3.ts) @[code](@/content/guides/cell-features/formatting-cells/angular/example3.html) ::: ::: ::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/cell-features/formatting-cells/vue/example3.vue) ::: ::: ### Apply borders progressively for large configurations Building a very large [`customBorders`](@/api/options.md#customborders) configuration before the first render can delay the initial paint. To render the grid first and apply the borders in background batches, set [`customBordersProgressive`](@/api/options.md#custombordersprogressive) to `true`: ```js const hot = new Handsontable(container, { data, customBorders: largeBorderConfig, customBordersProgressive: true, licenseKey: 'non-commercial-and-evaluation', }); ``` Pass an object to control how many border entries are applied per batch: ```js customBordersProgressive: { chunkSize: 5000 }, ``` With progressive application enabled, the borders fill in after the grid becomes interactive, so [`getBorders()`](@/api/customBorders.md#getborders) and the cells' border metadata are complete only once the [`afterCustomBordersUpdate`](@/api/hooks.md#aftercustombordersupdate) hook fires: ```js const hot = new Handsontable(container, { data, customBorders: largeBorderConfig, customBordersProgressive: true, licenseKey: 'non-commercial-and-evaluation', afterCustomBordersUpdate() { // every custom border is now applied }, }); ``` ## Result The grid renders your configured classes, inline styles, and border definitions. Formatting stays consistent after each render. ## Related articles **Related guides**
- [Conditional formatting](@/guides/cell-features/conditional-formatting/conditional-formatting.md)
**Configuration options**
- [activeHeaderClassName](@/api/options.md#activeheaderclassname) - [className](@/api/options.md#classname) - [commentedCellClassName](@/api/options.md#commentedcellclassname) - [currentColClassName](@/api/options.md#currentcolclassname) - [currentHeaderClassName](@/api/options.md#currentheaderclassname) - [currentRowClassName](@/api/options.md#currentrowclassname) - [customBorders](@/api/options.md#customborders) - [customBordersProgressive](@/api/options.md#custombordersprogressive) - [invalidCellClassName](@/api/options.md#invalidcellclassname) - [noWordWrapClassName](@/api/options.md#nowordwrapclassname) - [placeholder](@/api/options.md#placeholder) - [placeholderCellClassName](@/api/options.md#placeholdercellclassname) - [readOnlyCellClassName](@/api/options.md#readonlycellclassname) - [tableClassName](@/api/options.md#tableclassname)
**Plugins**
- [CustomBorders](@/api/customBorders.md)
--- # Cell functions URL: https://handsontable.com/docs/javascript-data-grid/cell-function/ A cell function is one of three components -- renderer, editor, or validator -- that controls how a cell displays, accepts, and validates data. [[toc]] ## Overview Every Handsontable cell has three associated functions that handle distinct concerns: | Function | Role | Implemented as | | --- | --- | --- | | [`renderer`](@/guides/cell-functions/cell-renderer/cell-renderer.md) | Controls how a cell looks: DOM structure, CSS classes, HTML content | A plain function | | [`editor`](@/guides/cell-functions/cell-editor/cell-editor.md) | Controls how a cell is edited: input element, keyboard handling, open/close lifecycle | A class extending `BaseEditor` | | [`validator`](@/guides/cell-functions/cell-validator/cell-validator.md) | Decides whether a cell value is acceptable | A function or `RegExp` | The three functions are **independent**. You can mix and match any combination: use the built-in numeric editor with a custom renderer, override just the validator while keeping a built-in type, or write all three from scratch. ### Function signatures ```js // renderer — called for every visible cell on every render renderer(hotInstance, td, row, col, prop, value, cellProperties) // hotInstance – Handsontable instance // td – HTMLTableCellElement to modify // row, col – visual row and column indexes // prop – data property name (string) or column index (number) // value – current cell value // cellProperties – merged cell configuration object // validator — may be synchronous or asynchronous validator(value, callback) // value – value to validate // callback – call with true (valid) or false (invalid) // RegExp alternative: /pattern/.test(value) must return true // editor — a class; see the Cell editor guide for the full lifecycle API class MyEditor extends BaseEditor { ... } ``` `validator` is **optional**. If no validator is defined for a cell, the cell is skipped entirely during validation — afterValidate will not fire for it, and it will not contribute to the validation cycle. ### `allowInvalid` By default, `allowInvalid: true` — invalid cells are accepted into the data source but marked with the `htInvalid` CSS class. Set `allowInvalid: false` to reject invalid values and keep the editor open until a valid value is entered. ## Cell types bundle all three A [cell type](@/guides/cell-types/cell-type/cell-type.md) is a preset that assigns a matching `renderer`, `editor`, and `validator` together under a single `type` alias. Using `type: 'numeric'` is shorthand for: ```js { renderer: Handsontable.renderers.NumericRenderer, editor: Handsontable.editors.NumericEditor, validator: Handsontable.validators.NumericValidator, } ``` Built-in types: `text`, `numeric`, `checkbox`, `date`, `time`, `dropdown`, `autocomplete`, `password`, `handsontable`. When you set an explicit `renderer`, `editor`, or `validator` alongside a `type`, the explicit function always takes precedence over the type for that function only: ```js columns: [{ type: 'numeric', // sets NumericEditor + NumericValidator renderer: myRenderer, // overrides only NumericRenderer; editor and validator stay numeric }] ``` **When to use a type vs individual functions:** - Use `type` when you want the standard, bundled behavior for a data kind (numbers, dates, checkboxes). - Override a single function from a type when one aspect needs customizing but the rest is fine as-is. - Set individual `renderer`/`editor`/`validator` directly when no built-in type fits or you need full control. ## Configuration priority Cell functions resolve using the cascading configuration model. The most specific level wins: ``` cell[row][col] > column > global (root settings) ``` ::: only-for javascript ```js new Handsontable(container, { type: 'text', // global fallback for all cells columns: [ { type: 'numeric' }, // overrides global for all cells in column 0 { type: 'text' }, // same as global for column 1 ], cell: [ { row: 0, col: 0, type: 'checkbox' }, // overrides column setting for cell [0, 0] only ], }); ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts settings: GridSettings = { type: 'text', // global fallback for all cells columns: [ { type: 'numeric' }, // overrides global for all cells in column 0 { type: 'text' }, // same as global for column 1 ], cell: [ { row: 0, col: 0, type: 'checkbox' }, // overrides column setting for cell [0, 0] only ], }; ``` ::: ::: only-for vue ```html ``` ::: ## Mixing renderer, editor, and validator The example below shows a product inventory table. Each column uses a different function configuration: - **Product** — `type: 'text'` bundles text renderer, text editor, and no validator. - **Price** — `type: 'numeric'` bundles numeric renderer (formatted as currency), numeric editor, and numeric validator. - **Stock** — custom `renderer` (progress bar), built-in `'numeric'` editor, and a custom range `validator`. All three come from different sources. ::: only-for javascript ::: example #example1 --js 1 --ts 2 --css 3 @[code](@/content/guides/cell-functions/cell-function/javascript/example1.js) @[code](@/content/guides/cell-functions/cell-function/javascript/example1.ts) @[code](@/content/guides/cell-functions/cell-function/javascript/example1.css) ::: ::: ::: only-for react ::: example #example1 :react --css 1 --js 2 --ts 3 @[code](@/content/guides/cell-functions/cell-function/react/example1.css) @[code](@/content/guides/cell-functions/cell-function/react/example1.jsx) @[code](@/content/guides/cell-functions/cell-function/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/cell-functions/cell-function/angular/example1.ts) @[code](@/content/guides/cell-functions/cell-function/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/cell-functions/cell-function/vue/example1.vue) ::: ::: Double-click any **Stock** cell to edit it with the numeric editor. The bar renderer updates on save. Enter a value outside 0–1000 to see the validator reject it (cell turns red when `allowInvalid: false`). ## Performance Renderers are called separately for every displayed cell on every table render. A table can render many times during its lifetime — after scrolling, sorting, editing, and more. Keep `renderer` functions as simple and fast as possible to avoid performance drops, especially with large datasets. ## Getting cell functions programmatically Use [`getCellMeta(row, col)`](@/api/core.md#getcellmeta) to read all properties of a cell at once, or the dedicated getters for individual functions: ::: only-for javascript ```js const cellProperties = hot.getCellMeta(0, 0); cellProperties.renderer; // renderer function cellProperties.editor; // editor class cellProperties.validator; // validator function or RegExp cellProperties.type; // cell type string ``` ::: ::: only-for react ```jsx const hot = hotRef.current.hotInstance; const cellProperties = hot.getCellMeta(0, 0); cellProperties.renderer; // renderer function cellProperties.editor; // editor class cellProperties.validator; // validator function or RegExp cellProperties.type; // cell type string ``` ::: ::: only-for angular ```ts const cellProperties = this.hotTable.hotInstance.getCellMeta(0, 0); const renderer = cellProperties.renderer; // renderer function const editor = cellProperties.editor; // editor class const validator = cellProperties.validator; // validator function or RegExp const type = cellProperties.type; // cell type string ``` ::: ::: only-for vue ```html ``` ::: Dedicated getters: | Method | Returns | | --- | --- | | [`getCellRenderer(row, col)`](@/api/core.md#getcellrenderer) | The resolved renderer function for the cell | | [`getCellEditor(row, col)`](@/api/core.md#getcelleditor) | The resolved editor class for the cell | | [`getCellValidator(row, col)`](@/api/core.md#getcellvalidator) | The resolved validator function or `RegExp` for the cell | If a cell's functions are defined through a cell type, the getters return the resolved functions, not the type string: ::: only-for javascript ```js const hot = new Handsontable(container, { columns: [{ type: 'numeric' }], }); const cellProperties = hot.getCellMeta(0, 0); cellProperties.renderer; // numericRenderer function cellProperties.editor; // NumericEditor class cellProperties.validator; // numericValidator function cellProperties.type; // 'numeric' ``` ::: ::: only-for react ::: tip To call these methods, access the Handsontable instance through the `HotTable` ref's `hotInstance` property. See [Instance methods](@/guides/getting-started/react-methods/react-methods.md) for details. ::: ```jsx const ExampleComponent = () => { const hotRef = useRef(null); useEffect(() => { const hot = hotRef.current.hotInstance; const cellProperties = hot.getCellMeta(0, 0); cellProperties.renderer; // numericRenderer function cellProperties.editor; // NumericEditor class cellProperties.validator; // numericValidator function cellProperties.type; // 'numeric' }); return ; }; ``` ::: ::: only-for angular ```ts import { Component, AfterViewInit, ViewChild } from '@angular/core'; import { HotTableComponent } from '@handsontable/angular-wrapper'; @Component({ selector: 'app-example', template: '', standalone: false, }) export class AppComponent implements AfterViewInit { @ViewChild(HotTableComponent) hotTable!: HotTableComponent; settings = { columns: [{ type: 'numeric' }], }; ngAfterViewInit() { const hot = this.hotTable.hotInstance; const cellProperties = hot.getCellMeta(0, 0); const renderer = cellProperties.renderer; // numericRenderer function const editor = cellProperties.editor; // NumericEditor class const validator = cellProperties.validator; // numericValidator function const type = cellProperties.type; // 'numeric' } } ``` ::: ::: only-for vue ```html ``` ::: ## Related articles **Related guides**
- [Cell editor](@/guides/cell-functions/cell-editor/cell-editor.md) - [Cell renderer](@/guides/cell-functions/cell-renderer/cell-renderer.md) - [Cell validator](@/guides/cell-functions/cell-validator/cell-validator.md) - [Cell type](@/guides/cell-types/cell-type/cell-type.md)
**Configuration options** - Configuration options: - [`editor`](@/api/options.md#editor) - [`renderer`](@/api/options.md#renderer) - [`type`](@/api/options.md#type) - [`validator`](@/api/options.md#validator) - [`allowInvalid`](@/api/options.md#allowinvalid) - [`valueFormatter`](@/api/options.md#valueformatter) - Core methods: - [`destroyEditor()`](@/api/core.md#destroyeditor) - [`getActiveEditor()`](@/api/core.md#getactiveeditor) - [`getCellEditor()`](@/api/core.md#getcelleditor) - [`getCellMeta()`](@/api/core.md#getcellmeta) - [`getCellMetaAtRow()`](@/api/core.md#getcellmetaatrow) - [`getCellsMeta()`](@/api/core.md#getcellsmeta) - [`getCellRenderer()`](@/api/core.md#getcellrenderer) - [`getCellValidator()`](@/api/core.md#getcellvalidator) - [`setCellMeta()`](@/api/core.md#setcellmeta) - [`setCellMetaObject()`](@/api/core.md#setcellmetaobject) - [`removeCellMeta()`](@/api/core.md#removecellmeta) - Hooks: - [`afterBeginEditing`](@/api/hooks.md#afterbeginediting) - [`afterGetCellMeta`](@/api/hooks.md#aftergetcellmeta) - [`afterGetColumnHeaderRenderers`](@/api/hooks.md#aftergetcolumnheaderrenderers) - [`afterGetRowHeaderRenderers`](@/api/hooks.md#aftergetrowheaderrenderers) - [`afterValidate`](@/api/hooks.md#aftervalidate) - [`afterRenderer`](@/api/hooks.md#afterrenderer) - [`beforeGetCellMeta`](@/api/hooks.md#beforegetcellmeta) - [`beforeRenderer`](@/api/hooks.md#beforerenderer) - [`beforeValidate`](@/api/hooks.md#beforevalidate) ## Related - [Cell renderer](@/guides/cell-functions/cell-renderer/cell-renderer.md) -- how to control cell display using renderer functions - [Cell editor](@/guides/cell-functions/cell-editor/cell-editor.md) -- how to control cell editing using editor classes - [Cell validator](@/guides/cell-functions/cell-validator/cell-validator.md) -- how to enforce data rules using validator functions --- # Cell renderer URL: https://handsontable.com/docs/javascript-data-grid/cell-renderer/ A cell renderer is a function that controls how cell content is displayed in the DOM. Override a built-in renderer or write your own to customize the visual output. [[toc]] ## Use a cell renderer You can use any of the built-in renderers by specifying their alias name in your column configuration. The example below shows how to use the `numeric` renderer, which formats numeric values according to the cell's formatting options: ::: only-for javascript ```js const container = document.querySelector("#container"); const hot = new Handsontable(container, { data: someData, columns: [ { renderer: "numeric", }, ], }); ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts settings = { columns: [ { renderer: "numeric", }, ] }; ``` ::: ::: only-for vue ```html ``` ::: ::: only-for react ## Declare a custom renderer as a component Handsontable's React wrapper lets you create custom cell renderers using React components. To use your component as a Handsontable renderer, pass it in the `renderer` prop if either `HotTable` or `HotColumn` components, as you would with any other config option. ::: tip Handsontable's [`autoRowSize`](@/api/options.md#autorowsize) and [`autoColumnSize`](@/api/options.md#autocolumnsize) options require calculating the widths/heights of some of the cells before rendering them into the table. For this reason, it's not currently possible to use them alongside component-based renderers, as they're created after the table's initialization. Be sure to turn those options off in your Handsontable configuration, as keeping them enabled may cause unexpected results. Please note that [`autoColumnSize`](@/api/options.md#autocolumnsize) is enabled by default. ::: ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/cell-functions/cell-renderer/react/example1.jsx) @[code](@/content/guides/cell-functions/cell-renderer/react/example1.tsx) ::: ## Use the renderer component within React's context In this example, React's `Context` passes information available in the main app component to the renderer. In this case, we're using just the renderer, but the same principle works with [editors](@/guides/cell-functions/cell-editor/cell-editor.md) as well. ::: example #example2 :react --css 1 --js 2 --ts 3 @[code](@/content/guides/cell-functions/cell-renderer/react/example2.css) @[code](@/content/guides/cell-functions/cell-renderer/react/example2.jsx) @[code](@/content/guides/cell-functions/cell-renderer/react/example2.tsx) ::: ## Declare a custom renderer as a function You can also declare a custom renderer for the `HotTable` component by declaring it as a function. In the simplest scenario, you can pass the rendering function as the `hotRenderer` prop into `HotTable` or `HotColumn`. If you need the renderer to be a part of a `columns` config array, declare it under the `renderer` key. The following example implements `@handsontable/react-wrapper` with a custom renderer added to one of the columns. It takes an image URL as the input and renders the image in the edited cell. ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/cell-functions/cell-renderer/react/example3.jsx) @[code](@/content/guides/cell-functions/cell-renderer/react/example3.tsx) ::: ::: ::: only-for angular ## Declare a custom renderer as a component Handsontable's Angular wrapper lets you create custom cell renderers using Angular components. To do so, create a component that extends the base renderer class `HotCellRendererComponent`. In the Angular wrapper for Handsontable, a renderer can be provided either as an Angular component or as a function. To use your component as a Handsontable renderer, pass it in the `renderer` prop to the `HotTableComponent`, as you would with any other config option. ::: tip Handsontable's [`autoRowSize`](@/api/options.md#autorowsize) and [`autoColumnSize`](@/api/options.md#autocolumnsize) options require calculating the widths/heights of some of the cells before rendering them into the table. For this reason, it's not currently possible to use them alongside component-based renderers, as they're created after the table's initialization. Be sure to turn those options off in your Handsontable configuration, as keeping them enabled may cause unexpected results. Please note that [`autoColumnSize`](@/api/options.md#autocolumnsize) is enabled by default. ::: ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/cell-functions/cell-renderer/angular/example1.ts) @[code](@/content/guides/cell-functions/cell-renderer/angular/example1.html) ::: You can create and use a custom cell renderer component that utilizes the `rendererProps` property and use them inside the renderer component. ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/cell-functions/cell-renderer/angular/example3.ts) @[code](@/content/guides/cell-functions/cell-renderer/angular/example3.html) ::: ## Declare a custom renderer as an Angular template The Angular wrapper supports using an Angular `TemplateRef` as a renderer. This is particularly useful if you want to leverage the power of Angular templates directly, without creating a full component. ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/cell-functions/cell-renderer/angular/example2.ts) @[code](@/content/guides/cell-functions/cell-renderer/angular/example2.html) ::: ## Declare a custom renderer as a function You can also declare a custom renderer for the `HotTable` component by declaring it as a function. In the simplest scenario, you can pass the rendering function as the `renderer` prop into `HotTableComponent`. The following example implements `@handsontable/angular-wrapper` with a custom renderer added to one of the columns. It takes an image URL as the input and renders the image in the edited cell. ::: example #example4 :angular --ts 1 --html 2 @[code](@/content/guides/cell-functions/cell-renderer/angular/example4.ts) @[code](@/content/guides/cell-functions/cell-renderer/angular/example4.html) ::: ::: ::: only-for vue ## Declare a custom renderer as a component Handsontable's Vue wrapper lets you create custom cell renderers using Vue components. To use a Vue component as a renderer, define the component with `defineComponent`, then write a renderer function that mounts it into the cell `td` element with Vue's `render` and `h` helpers. Mounting with `render(h(Component, props), td)` reuses the same component instance across re-renders -- Vue patches the existing tree instead of remounting it. To pass static props alongside the cell data, merge them into the second argument of `h()`. ::: tip Handsontable's [`autoRowSize`](@/api/options.md#autorowsize) and [`autoColumnSize`](@/api/options.md#autocolumnsize) options require calculating the widths/heights of some of the cells before rendering them into the table. For this reason, it's not currently possible to use them alongside component-based renderers, as they're created after the table's initialization. Be sure to turn those options off in your Handsontable configuration, as keeping them enabled may cause unexpected results. Please note that [`autoColumnSize`](@/api/options.md#autocolumnsize) is enabled by default. ::: ::: example #example1 :vue3 @[code](@/content/guides/cell-functions/cell-renderer/vue/example1.vue) ::: If your component needs access to a Vue application context -- for example, global components, plugins, or `provide` / `inject` -- use `createApp(Component, props).mount(td)` instead of `render()`. Store a reference to the mounted app on the `td` element and call `app.unmount()` at the start of each render call to avoid leaking app instances: ::: example #example2 :vue3 @[code](@/content/guides/cell-functions/cell-renderer/vue/example2.vue) ::: ::: ::: only-for react angular ::: tip All the sections below describe how to utilize the features available for the Handsontable function-based renderers. ::: ::: ## Register custom cell renderer To register your own alias use `registerRenderer()` function from the `handsontable/renderers` module. It takes two arguments: - `rendererName` - a string representing a renderer function - `renderer` - a renderer function that will be represented by `rendererName` If you'd like to register `asteriskDecoratorRenderer` under alias `asterisk` you have to call: ```js import { registerRenderer } from "handsontable/renderers"; registerRenderer("asterisk", asteriskDecoratorRenderer); ``` ::: only-for angular ::: tip When using `registerRenderer()`, remember to call it at startup (e.g. in `main.ts` or `AppModule`), not in the component constructor. This ensures the renderer is registered before the table is initialized. ::: ::: Choose aliases wisely. If you register your renderer under name that is already registered, the target function will be overwritten: ```js import { registerRenderer } from "handsontable/renderers"; registerRenderer("text", asteriskDecoratorRenderer); ``` Now `"text"` alias points to `asteriskDecoratorRenderer` function, not the built-in `textRenderer`. So, unless you intentionally want to overwrite an existing alias, try to choose a unique name. A good practice is prefixing your aliases with some custom name (for example your GitHub username) to minimize the possibility of name collisions. This is especially important if you want to publish your renderer, because you never know aliases has been registered by the user who uses your renderer. ```js import { registerRenderer } from "handsontable/renderers"; registerRenderer("asterisk", asteriskDecoratorRenderer); ``` Someone might already registered such alias ```js import { registerRenderer } from "handsontable/renderers"; registerRenderer("my.asterisk", asteriskDecoratorRenderer); ``` That's better. ## Use an alias The final touch is to use registered aliases. That way users can easily refer to an alias without the need to know the name of the function. To sum up, a well prepared renderer function should look like this: ```js import { registerRenderer } from "handsontable/renderers"; function customRenderer( hotInstance, td, row, column, prop, value, cellProperties ) { // ...your custom logic of the renderer } // Register an alias registerRenderer("my.custom", customRenderer); ``` From now on, you can use `customRenderer` like so: ::: only-for javascript ```js const container = document.querySelector("#container"); const hot = new Handsontable(container, { data: someData, columns: [ { renderer: "my.custom", }, ], }); ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts settings = { columns: [ { renderer: "my.custom", }, ] }; ``` ::: ::: only-for vue ```html ``` ::: When your custom renderer should preserve the default text output, call the built-in `textRenderer()` first. See [Extend a built-in renderer](#extend-a-built-in-renderer). ## Extend a built-in renderer When you build on top of a built-in renderer, Handsontable does not call that base renderer for you. You call it inside your custom renderer before your extra logic. Use `textRenderer` as the base when you want plain-text output and then apply styling or additional DOM changes. Use `htmlRenderer` as the base when your output is trusted HTML and you intentionally render with `innerHTML`. Skip a base renderer when your renderer fully controls cell output from scratch, for example the image-based `coverRenderer` in [Render custom HTML in cells](#render-custom-html-in-cells). Both of the following call styles are valid: ```js // Legacy style, common in classic JavaScript examples. textRenderer.apply(this, arguments); // Direct invocation style, common in ESM and TypeScript examples. textRenderer(instance, td, row, column, prop, value, cellProperties); ``` ## Render custom HTML in cells This example shows how to use custom cell renderers to display HTML content in a cell. This is a very powerful feature. Just remember to escape any HTML code that could be used for XSS attacks. ::: warning Security Handsontable does not include a built-in HTML sanitizer. When rendering untrusted user HTML, you must supply your own sanitizer via the [`sanitizer`](@/api/options.md#sanitizer) option. Without it, rendering untrusted HTML creates XSS vulnerabilities. See [Security](@/guides/security/security/security.md) for details. ::: In the below configuration: - **Title** column uses built-in HTML renderer that allows any HTML. This is unsafe if your data comes from an untrusted source. A Handsontable user can enter ` ``` ::: the functions [`editor`](@/api/options.md#editor), [`renderer`](@/api/options.md#renderer), and [`copyable`](@/api/options.md#copyable) are automatically set as follows: ::: only-for javascript ```js columns: [{ editor: Handsontable.editors.PasswordEditor renderer: Handsontable.renderers.PasswordRenderer, copyable: false, }] ``` ::: ::: only-for react ```jsx columns={[{ editor: Handsontable.editors.PasswordEditor renderer: Handsontable.renderers.PasswordRenderer, copyable: false, }]} ``` ::: ::: only-for angular ```ts settings = { columns: [ { editor: Handsontable.editors.PasswordEditor renderer: Handsontable.renderers.PasswordRenderer, copyable: false, }, ], }; ``` ::: ::: only-for vue ```vue ``` ::: Is an equivalent to defining them all: ::: only-for javascript ```js columns: [ { editor: false, renderer: Handsontable.renderers.TextRenderer, className: "my-cell", readOnly: true, myCustomProperty: "foo", }, ]; ``` ::: ::: only-for react ```jsx columns={[{ editor: false, renderer: Handsontable.renderers.TextRenderer, className: 'my-cell', readOnly: true, myCustomProperty: 'foo', }]} ``` ::: ::: only-for angular ```ts settings = { columns: [ { editor: false, renderer: Handsontable.renderers.TextRenderer, className: "my-cell", readOnly: true, myCustomProperty: "foo", }, ], }; ``` ::: ::: only-for vue ```vue ``` ::: ## Register custom cell type When you create a custom cell type, best practice is to assign it as an alias that will refer to this particular type definition. This gives users a convenient way of defining which cell type should be used for describing cell properties. The user doesn't need to know which part of the code is responsible for rendering, validating, or editing the cell value. They do not even need to know that there are any functions at all. You can change the cell behaviour associated with an alias without changing the code that defines a cell's properties. To register your own alias use `Handsontable.cellTypes.registerCellType()` function. It takes two arguments: - `cellTypeName` - a string representing the cell type object - [`type`](@/api/options.md#type) - an object represented by `cellTypeName`. It can include cell behavior callbacks, such as [`editor`](@/api/options.md#editor), [`renderer`](@/api/options.md#renderer), and [`validator`](@/api/options.md#validator). It can also include Handsontable settings, such as [`readOnly`](@/api/options.md#readonly), [`className`](@/api/options.md#classname), and [`allowInvalid`](@/api/options.md#allowinvalid), or custom properties, such as `myCustomCellState`, available through `cellProperties`. If you'd like to register `copyablePasswordType` under alias `copyable-password`, you need to call: ```js Handsontable.cellTypes.registerCellType('copyable-password', { editor: copyablePasswordEditor, renderer: copyablePasswordRenderer, }); ``` Choose aliases wisely. If you register your cell type under name that is already registered, the target function will be overwritten: ```js Handsontable.cellTypes.registerCellType('password', { editor: copyablePasswordEditor, renderer: copyablePasswordRenderer, }); // Now 'password' alias points to the newly created // object, not Handsontable.cellTypes.password ``` Unless you intentionally want to overwrite an existing alias, try to choose a unique name. Best practice is to prefix your aliases with a custom name to minimize the possibility of name collisions. This is especially important if you want to publish your cell type as you never know what aliases have been registered by a user who uses your cell type. ```js Handsontable.cellTypes.registerCellType('copyable-password', { editor: copyablePasswordEditor, renderer: copyablePasswordRenderer, }); ``` Someone might already registered such alias. It would be better use a unique prefix: ```js Handsontable.cellTypes.registerCellType('my.copyable-password', { editor: copyablePasswordEditor, renderer: copyablePasswordRenderer, }); ``` To sum up, a well-prepared cell type object should look like this: ```js class MyEditor extends Handsontable.editors.TextEditor {} function customRenderer(instance, td, row, column, prop, value, cellProperties) { // ...renderer logic } function customValidator(query, callback) { // ...validator logic callback(/* Pass `true` or `false` */); } // Register an alias Handsontable.cellTypes.registerCellType('my.custom', { editor: MyEditor, renderer: customRenderer, validator: customValidator, // You can add additional options to the cell type // based on Handsontable settings className: 'my-cell', allowInvalid: true, // Or you can add custom properties which // will be accessible in `cellProperties` myCustomCellState: 'complete', }); ``` ## Using an alias The next step is to use the registered aliases to enable users to easily refer to them without the need to know what the actual cell type object is. Here's an example of how you would use your cell definition: ::: only-for javascript ```js const hot = new Handsontable(container, { columns: [{ type: 'my.custom' }] }); ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts settings = { columns: [ { type: "my.custom", }, ], }; ``` ```html ``` ::: ::: only-for vue ```vue ``` ::: ## Precedence It is possible to define the [`type`](@/api/options.md#type) option together with options such as [`renderer`](@/api/options.md#renderer), [`editor`](@/api/options.md#editor) or [`validator`](@/api/options.md#validator). For example: ::: only-for javascript ```js const hot = new Handsontable(container, { columns: [{ type: 'numeric', // validator function defined elsewhere validator: customValidator }] }); ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts settings = { columns: [ { type: "numeric", // validator function defined elsewhere validator: customValidator, }, ], }; ``` ```html ``` ::: ::: only-for vue ```vue ``` ::: We defined the[`type`](@/api/options.md#type) for all cells in a column to be `numeric`. We also defined a validator function directly. In Handsontable, cell functions that are defined directly always take precedence over functions associated with cell type, so the above configuration is equivalent to: ::: only-for javascript ```js const hot = new Handsontable(container, { columns: [{ renderer: Handsontable.renderers.TextRenderer, editor: Handsontable.editors.TextEditor, validator: customValidator }] }); ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts settings = { columns: [ { renderer: Handsontable.renderers.TextRenderer, editor: Handsontable.editors.TextEditor, validator: customValidator, }, ], }; ``` ```html ``` ::: ::: only-for vue ```vue ``` ::: There is one more way you can define the configuration using types: ::: only-for javascript ```js const hot = new Handsontable(container, { // validator function defined elsewhere validator: customValidator, columns: [{ type: 'my.custom' }] }); ``` ::: ::: only-for react ```jsx ``` ::: ::: only-for angular ```ts settings = { validator: customValidator, columns: [ { renderer: Handsontable.renderers.TextRenderer, editor: Handsontable.editors.TextEditor, validator: customValidator, }, ], }; ``` ```html ``` ::: ::: only-for vue ```vue ``` ::: Using [cascade configuration](@/guides/getting-started/configuration-options/configuration-options.md#cascading-configuration) we define a table with two columns, with [`validator`](@/api/options.md#validator) set to `customValidator` function. The s[`type`](@/api/options.md#type) of the first column is set to `password`. The `Password` cell type does not define a validator function: ```js { renderer: Handsontable.renderers.PasswordRenderer, editor: Handsontable.editors.PasswordEditor, validator: undefined } ``` Because `type: 'password'` is a more specific configuration for the cells in the first column than the `validator: customValidator`, cell functions associated with the `password` type take precedence over the functions defined on the higher level of configuration. Therefore, the equivalent configuration is: ::: only-for javascript ```js function customValidator(query, callback) { // ...validator logic callback(/* Pass `true` or `false` */); } const hot = new Handsontable(container, { columns: [{ renderer: Handsontable.renderers.PasswordRenderer, editor: Handsontable.editors.PasswordEditor, validator: undefined }, { renderer: Handsontable.renderers.TextRenderer, editor: Handsontable.editors.TextEditor, validator: customValidator }] }); ``` ::: ::: only-for react ```jsx function customValidator(query, callback) { // ...validator logic callback(/* Pass `true` or `false` */); } ; ``` ::: ::: only-for angular ```ts function customValidator(query, callback) { // ...validator logic callback(/* Pass `true` or `false` */); } settings = { validator: customValidator, columns: [ { renderer: Handsontable.renderers.TextRenderer, editor: Handsontable.editors.TextEditor, validator: customValidator, }, ], }; ``` ```html ``` ::: ::: only-for vue ```vue ``` ::: ## Built-in cell types example The example below shows some of the built-in cell types, i.e. combinations of cell renderers and editors available in Handsontable. The example also shows the declaration of custom cell renderers, namely `yellowRenderer` and `greenRenderer`. ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/cell-types/cell-type/javascript/example1.js) @[code](@/content/guides/cell-types/cell-type/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/cell-types/cell-type/react/example1.jsx) @[code](@/content/guides/cell-types/cell-type/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/cell-types/cell-type/angular/example1.ts) @[code](@/content/guides/cell-types/cell-type/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/cell-types/cell-type/vue/example1.vue) ::: ::: ## Empty cells It's worth to mention that values such as `''` (empty string), `null` and `undefined` are considered empty values. Cells with empty values are displayed in a similar way for most of the data types (see below). ::: tip Please keep in mind that opening a cell with `undefined` and `null` values results in overwriting the original value with an empty string. Moreover, copying and pasting that values will result in pasting the empty string. ::: ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/cell-types/cell-type/javascript/example2.js) @[code](@/content/guides/cell-types/cell-type/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/cell-types/cell-type/react/example2.jsx) @[code](@/content/guides/cell-types/cell-type/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/cell-types/cell-type/angular/example2.ts) @[code](@/content/guides/cell-types/cell-type/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/cell-types/cell-type/vue/example2.vue) ::: ::: Empty cells may be treated differently in different contexts, for example, the [`ColumnSorting`](@/api/columnSorting.md) plugin has `sortEmptyCells` option which is responsible for establishing whether empty cells should be sorted like non-empty cells. ## Set different cell types in one column Use the [`cells`](@/api/options.md#cells) option to assign a different cell type to each row in a column. The following example creates a project settings form with `dropdown`, `numeric`, `intl-date`, `checkbox`, `text`, `handsontable`, and `password` cell types. :::: only-for javascript ::: example #example3 --js 1 --ts 2 @[code](@/content/guides/cell-types/cell-type/javascript/example3.js) @[code](@/content/guides/cell-types/cell-type/javascript/example3.ts) ::: :::: :::: only-for react ::: example #example3 :react --js 1 --ts 2 @[code](@/content/guides/cell-types/cell-type/react/example3.jsx) @[code](@/content/guides/cell-types/cell-type/react/example3.tsx) ::: :::: :::: only-for angular ::: example #example3 :angular --ts 1 --html 2 @[code](@/content/guides/cell-types/cell-type/angular/example3.ts) @[code](@/content/guides/cell-types/cell-type/angular/example3.html) ::: :::: :::: only-for vue ::: example #example3 :vue3 @[code](@/content/guides/cell-types/cell-type/vue/example3.vue) ::: :::: ## Related
- [Autocomplete cell type](@/guides/cell-types/autocomplete-cell-type/autocomplete-cell-type.md) - [Checkbox cell type](@/guides/cell-types/checkbox-cell-type/checkbox-cell-type.md) - [Date cell type](@/guides/cell-types/date-cell-type/date-cell-type.md) - [Dropdown cell type](@/guides/cell-types/dropdown-cell-type/dropdown-cell-type.md) - [Handsontable cell type](@/guides/cell-types/handsontable-cell-type/handsontable-cell-type.md) - [MultiSelect cell type](@/guides/cell-types/multiselect-cell-type/multiselect-cell-type.md) - [Numeric cell type](@/guides/cell-types/numeric-cell-type/numeric-cell-type.md) - [Password cell type](@/guides/cell-types/password-cell-type/password-cell-type.md) - [Select cell type](@/guides/cell-types/select-cell-type/select-cell-type.md) - [Text cell type](@/guides/cell-types/text-cell-type/text-cell-type.md) - [Time cell type](@/guides/cell-types/time-cell-type/time-cell-type.md)
## Related articles **Related guides**
- [Cell function](@/guides/cell-functions/cell-function/cell-function.md) - [Cell editor](@/guides/cell-functions/cell-editor/cell-editor.md) - [Cell renderer](@/guides/cell-functions/cell-renderer/cell-renderer.md) - [Cell validator](@/guides/cell-functions/cell-validator/cell-validator.md)
**Configuration options**
- [editor](@/api/options.md#editor) - [renderer](@/api/options.md#renderer) - [type](@/api/options.md#type) - [validator](@/api/options.md#validator)
**Core methods**
- [getCellEditor()](@/api/core.md#getcelleditor) - [getCellMeta()](@/api/core.md#getcellmeta) - [getCellMetaAtRow()](@/api/core.md#getcellmetaatrow) - [getCellsMeta()](@/api/core.md#getcellsmeta) - [getCellRenderer()](@/api/core.md#getcellrenderer) - [getCellValidator()](@/api/core.md#getcellvalidator) - [getDataType()](@/api/core.md#getdatatype) - [setCellMeta()](@/api/core.md#setcellmeta) - [setCellMetaObject()](@/api/core.md#setcellmetaobject) - [removeCellMeta()](@/api/core.md#removecellmeta)
**Hooks**
- [afterBeginEditing](@/api/hooks.md#afterbeginediting) - [afterGetCellMeta](@/api/hooks.md#aftergetcellmeta) - [afterGetColumnHeaderRenderers](@/api/hooks.md#aftergetcolumnheaderrenderers) - [afterGetRowHeaderRenderers](@/api/hooks.md#aftergetrowheaderrenderers) - [afterSetCellMeta](@/api/hooks.md#aftersetcellmeta) - [afterValidate](@/api/hooks.md#aftervalidate) - [afterRenderer](@/api/hooks.md#afterrenderer) - [beforeGetCellMeta](@/api/hooks.md#beforegetcellmeta) - [beforeRenderer](@/api/hooks.md#beforerenderer) - [beforeSetCellMeta](@/api/hooks.md#beforesetcellmeta) - [beforeValidate](@/api/hooks.md#beforevalidate)
--- # Text cell type URL: https://handsontable.com/docs/javascript-data-grid/text-cell-type/ Use the text cell type, the default cell type in Handsontable, to display and edit plain text values. [[toc]] ## Overview The text cell type is the default [cell type](@/guides/cell-types/cell-type/cell-type.md) in Handsontable. It renders a cell's value as plain text and lets you edit it with a standard text input. It has no built-in validator. Because `text` is the default, you don't need to set `type: 'text'` for it to apply. Set it explicitly when you want to override a different type inherited from a higher configuration level, or when you want the configuration to state the type directly, for example to guard alphanumeric codes or values with leading zeros against being reformatted by another type. ::: only-for javascript ::: example #example1 --js 1 --ts 2 @[code](@/content/guides/cell-types/text-cell-type/javascript/example1.js) @[code](@/content/guides/cell-types/text-cell-type/javascript/example1.ts) ::: ::: ::: only-for react ::: example #example1 :react --js 1 --ts 2 @[code](@/content/guides/cell-types/text-cell-type/react/example1.jsx) @[code](@/content/guides/cell-types/text-cell-type/react/example1.tsx) ::: ::: ::: only-for angular ::: example #example1 :angular --ts 1 --html 2 @[code](@/content/guides/cell-types/text-cell-type/angular/example1.ts) @[code](@/content/guides/cell-types/text-cell-type/angular/example1.html) ::: ::: ::: only-for vue ::: example #example1 :vue3 @[code](@/content/guides/cell-types/text-cell-type/vue/example1.vue) ::: ::: In the example above, the `sku` and `category` columns use `type: 'text'` explicitly, even though it's the default. This makes the configuration self-documenting: values such as `'004821'` are alphanumeric codes with leading zeros, not numbers, and the explicit type states that intent even if a later change sets a different type at the grid or column level. ## Adding a validator The text cell type ships without a built-in [validator](@/guides/cell-functions/cell-validator/cell-validator.md). To validate text values, combine `type: 'text'` with your own validator function, and set [`allowInvalid`](@/api/options.md#allowinvalid) to `false` to reject invalid entries. With `allowInvalid: false`, the cell editor doesn't close on an invalid value. It stays open until you either enter a value that passes validation or press **Esc** to cancel the edit and restore the previous value. See [invalid cell commit semantics](@/guides/cell-functions/cell-validator/cell-validator.md#invalid-cell-commit-semantics-vs-visual-marking) for the full behavior, including how it differs from `allowInvalid: true`. ::: only-for javascript ::: example #example2 --js 1 --ts 2 @[code](@/content/guides/cell-types/text-cell-type/javascript/example2.js) @[code](@/content/guides/cell-types/text-cell-type/javascript/example2.ts) ::: ::: ::: only-for react ::: example #example2 :react --js 1 --ts 2 @[code](@/content/guides/cell-types/text-cell-type/react/example2.jsx) @[code](@/content/guides/cell-types/text-cell-type/react/example2.tsx) ::: ::: ::: only-for angular ::: example #example2 :angular --ts 1 --html 2 @[code](@/content/guides/cell-types/text-cell-type/angular/example2.ts) @[code](@/content/guides/cell-types/text-cell-type/angular/example2.html) ::: ::: ::: only-for vue ::: example #example2 :vue3 @[code](@/content/guides/cell-types/text-cell-type/vue/example2.vue) ::: ::: ## Result After configuring the text cell type, cells display and edit their value as plain text, with no formatting or masking applied. Combine it with a custom [`validator`](@/api/options.md#validator) to restrict which values a text cell accepts. ## Keyboard shortcuts The text cell editor uses the standard [edition keyboard shortcuts](@/guides/navigation/keyboard-shortcuts/keyboard-shortcuts.md#edition-keyboard-shortcuts). It has no type-specific key bindings. ## Related articles **Related guides**
- [Cell type](@/guides/cell-types/cell-type/cell-type.md) - [Cell validator](@/guides/cell-functions/cell-validator/cell-validator.md)
**Configuration options**
- [type](@/api/options.md#type) - [validator](@/api/options.md#validator) - [allowInvalid](@/api/options.md#allowinvalid)
**Core methods**
- [getCellMeta()](@/api/core.md#getcellmeta) - [getCellMetaAtRow()](@/api/core.md#getcellmetaatrow) - [getCellsMeta()](@/api/core.md#getcellsmeta) - [getDataType()](@/api/core.md#getdatatype) - [setCellMeta()](@/api/core.md#setcellmeta) - [setCellMetaObject()](@/api/core.md#setcellmetaobject) - [removeCellMeta()](@/api/core.md#removecellmeta)
**Hooks**
- [afterGetCellMeta](@/api/hooks.md#aftergetcellmeta) - [afterSetCellMeta](@/api/hooks.md#aftersetcellmeta) - [afterValidate](@/api/hooks.md#aftervalidate) - [beforeGetCellMeta](@/api/hooks.md#beforegetcellmeta) - [beforeSetCellMeta](@/api/hooks.md#beforesetcellmeta) - [beforeValidate](@/api/hooks.md#beforevalidate)
--- # Numeric cell type URL: https://handsontable.com/docs/javascript-data-grid/numeric-cell-type/ Display, format, sort, and filter numbers correctly by using the numeric cell type. The numeric cell type formats numbers using Intl.NumberFormat, right-aligns values, and validates input, marking non-numeric values as invalid. [[toc]] ## 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 `