Skip to content

Row headers

Use default row headers (1, 2, 3), or set them to custom values provided by an array or a function.

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 to true.

Row headers as an array

An array of labels can be used to set the rowHeaders as shown in the example below:

Vue
<script setup lang="ts">
import { ref } from 'vue';
import { HotTable } from '@handsontable/vue3';
import { registerAllModules } from 'handsontable/registry';
import type { GridSettings } from 'handsontable/settings';
// register Handsontable's modules
registerAllModules();
const hotSettings = ref<GridSettings>({
data: [
[42000, 31000, 11000],
[45500, 33200, 12300],
[48700, 35100, 13600],
[51200, 36800, 14400],
[54800, 38900, 15900],
[57300, 40100, 17200],
],
colHeaders: ['Revenue', 'Expenses', 'Profit'],
rowHeaders: ['January', 'February', 'March', 'April', 'May', 'June'],
rowHeaderWidth: 80,
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
licenseKey: 'non-commercial-and-evaluation',
});
</script>
<template>
<div id="example2">
<HotTable :settings="hotSettings" />
</div>
</template>

Row headers as a function

The rowHeaders can also be populated using a function as shown in the example below:

Vue
<script setup lang="ts">
import { ref } from 'vue';
import { HotTable } from '@handsontable/vue3';
import { registerAllModules } from 'handsontable/registry';
import type { GridSettings } from 'handsontable/settings';
// register Handsontable's modules
registerAllModules();
const hotSettings = ref<GridSettings>({
data: [
[42000, 31000, 11000],
[45500, 33200, 12300],
[48700, 35100, 13600],
[51200, 36800, 14400],
[54800, 38900, 15900],
[57300, 40100, 17200],
],
colHeaders: ['Revenue', 'Expenses', 'Profit'],
rowHeaders: (index) => {
return `Row ${index + 1}`;
},
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
licenseKey: 'non-commercial-and-evaluation',
});
</script>
<template>
<div id="example3">
<HotTable :settings="hotSettings" />
</div>
</template>

Row header width

Row headers have a fixed width. When you use custom row labels, a label longer than that width is clipped, and the header does not grow to fit it the way a column does.

You have two ways to deal with this: set the width yourself, or let Handsontable measure it.

Set the width yourself

To control the header size, set rowHeaderWidth to one of the following:

  • A number - set the same width for every row header.
  • A string - set the same width, written as a pixel size: '80' or '80px'.
  • An array - set different widths for individual row header levels.

The width is a number of pixels. A string that states a pixel size works too, so a value coming from an attribute, a JSON config, or a framework template still applies. You can mix both forms inside the array. A value that states no pixel count, such as '50%' or '20em', is ignored - the header keeps its default width, and Handsontable reports the value once in the browser console. Inside an array this applies per level, so one unreadable entry does not disturb the levels around it.

A negative number is kept as it is, because numbers behave exactly as they did before this option read strings. A negative string is rejected, so a typo cannot collapse the header.

The Row headers as an array example uses custom labels together with rowHeaderWidth: 80.

Size the header to its content

To size the row header column to its longest label, enable the AutoRowHeaderSize plugin by setting autoRowHeaderSize to true. That is all you need: the plugin takes the header’s width over, so any rowHeaderWidth you already set is ignored while it is enabled.

In the example below, the row labels are too long for the default header width. Turning the plugin on is enough to make every label fit.

Vue
<script setup lang="ts">
import { ref } from 'vue';
import { HotTable } from '@handsontable/vue3';
import { registerAllModules } from 'handsontable/registry';
import type { GridSettings } from 'handsontable/settings';
// register Handsontable's modules
registerAllModules();
const hotSettings = ref<GridSettings>({
data: [
[42000, 45500, 48700, 51200],
[18300, 19100, 20400, 21600],
[23700, 26400, 28300, 29600],
[9800, 10200, 11100, 11700],
[13900, 16200, 17200, 17900],
[11200, 13100, 13900, 14500],
],
colHeaders: ['Q1', 'Q2', 'Q3', 'Q4'],
rowHeaders: [
'Revenue',
'Cost of goods sold',
'Gross profit',
'Operating expenses',
'Operating income',
'Net income',
],
// Size the row header column to its longest label.
autoRowHeaderSize: true,
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
licenseKey: 'non-commercial-and-evaluation',
});
</script>
<template>
<div id="example4">
<HotTable :settings="hotSettings" />
</div>
</template>

The plugin is off by default, because turning it on would change the row header width of every grid that uses custom labels. This mirrors AutoRowSize, which is also opt-in, while AutoColumnSize - bounded by the number of columns - is on by default.

The plugin never makes a header narrower than the default width, so a grid of short labels looks the same as it does without the plugin. It also leaves a little room around the longest label, so the text never sits flush against the cell border. That matters most for a row header you draw yourself: the grid’s own renderer wraps its label in a padded element, but a renderer pushed through afterGetRowHeaderRenderers writes straight into the cell and has no padding of its own.

Tuning the measurement

Instead of true, you can pass an object to change how the measurement runs. All three properties are optional, and the defaults suit most grids:

PropertyPossible valuesDescription
samplingRatioA numberHow many labels of the same length get rendered and measured. Default: 3.
allowSampleDuplicatestrue | falseWhether two rows carrying the same label are both measured. Default: false.
syncLimitA number | a percent stringHow many rows are read before the first paint. Default: 500.
autoRowHeaderSize: {
samplingRatio: 5,
allowSampleDuplicates: true,
syncLimit: 1000,
},

samplingRatio exists because reading a label is cheap but laying one out is not. The plugin groups the labels by how many characters they have and renders only a few from each group. Raising the number measures more of them, which is slightly slower but harder to fool: in a proportional font WWW is wider than iii, so measuring more same-length labels makes it less likely that the widest one is missed. Lower it to do less work.

allowSampleDuplicates decides what happens when two rows carry the same label. By default the label is measured once, because the same text normally renders to the same width. Set it to true when that is not true of your grid - a row header that is indented per row, as nestedRows does, draws the same label at a different width depending on how deep the row sits, so measuring only the first one would come out too narrow.

Raise samplingRatio along with it. Labels are grouped by length and only samplingRatio of each group get measured, so at the default of 3 a fourth row carrying the same label is still left out

  • which on a deep tree can be the widest one of them.

syncLimit decides how much of the work happens before the grid first appears. Finding the longest label means reading every row header once, which on a large grid takes long enough to be felt. So the first syncLimit rows are read straight away, and the rest are read in the browser’s idle time, in short bursts of a few milliseconds each - however many rows fit, which depends on how many row header columns the grid draws. A header can therefore widen a moment after the grid appears. While that is running a header only ever widens, so its width never jumps back and forth. Raise the limit to have more of it settled before the first paint, or pass a percent string such as '40%' to scale it with the number of rows.

Editing a cell does not start that work again. Only the rows that changed are read, because a row header label can be built from cell values - a data column used as the label, for instance. That reading also waits for an idle moment, so a header widened by an edit grows a moment after the edit rather than during it.

An edit can make a header wider, but never narrower. Working out that a header should shrink means finding the new longest label, which no shortcut avoids, so it waits for the next full pass. Loading or replacing the data starts one, as does adding or removing a row, sorting, hiding or showing a row, switching the theme, and a recalculation by the Formulas plugin.

Setting autoRowHeaderSize to false, or leaving it out, keeps the fixed-width headers.

Add more row header columns

A grid can render more than one row header. The afterGetRowHeaderRenderers hook hands you the array of renderers that draw them, one renderer per column. Push your own renderer onto that array, and it draws one more row header column to the right of the previous one.

The example below pushes two renderers next to the default numbering, so the grid gets three row header columns. Each column holds labels of a different length, and autoRowHeaderSize measures every column on its own: the numbering column stays narrow, while each label column gets the width its own text needs. Turn the plugin off and all three columns fall back to the same fixed width, which clips the longer labels.

Vue
<script setup lang="ts">
import { ref } from 'vue';
import { HotTable } from '@handsontable/vue3';
import { registerAllModules } from 'handsontable/registry';
import type { GridSettings } from 'handsontable/settings';
// register Handsontable's modules
registerAllModules();
const groups = ['Top line', 'Direct costs', 'Margin', 'Overhead', 'Margin', 'Bottom line'];
const lineItems = [
'Revenue',
'Cost of goods sold',
'Gross profit',
'Operating expenses',
'Operating income',
'Net income',
];
const hotSettings = ref<GridSettings>({
data: [
[42000, 45500, 48700, 51200],
[18300, 19100, 20400, 21600],
[23700, 26400, 28300, 29600],
[9800, 10200, 11100, 11700],
[13900, 16200, 17200, 17900],
[11200, 13100, 13900, 14500],
],
colHeaders: ['Q1', 'Q2', 'Q3', 'Q4'],
rowHeaders: true,
// Add two more row header columns next to the numbering.
afterGetRowHeaderRenderers: (renderers: Array<(renderableRowIndex: number, TH: HTMLElement) => void>) => {
// A renderer gets the renderable row index. It matches the visual index when no rows are hidden.
renderers.push((renderableRowIndex, TH) => {
TH.innerText = groups[renderableRowIndex];
});
renderers.push((renderableRowIndex, TH) => {
TH.innerText = lineItems[renderableRowIndex];
});
},
// Measure each of the three row header columns on its own.
autoRowHeaderSize: true,
height: 'auto',
autoWrapRow: true,
autoWrapCol: true,
licenseKey: 'non-commercial-and-evaluation',
});
</script>
<template>
<div id="example5">
<HotTable :settings="hotSettings" />
</div>
</template>

Each renderer is called with the row’s renderable index - the index Handsontable renders by. It is the same as the visual index until rows are hidden. If your grid hides rows, translate the index with getVisualFromRenderableIndex() before you look a label up by it.

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 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.
Vue
<script setup lang="ts">
import { ref } from 'vue';
import { HotTable } from '@handsontable/vue3';
import { registerAllModules } from 'handsontable/registry';
import type { GridSettings } from 'handsontable/settings';
registerAllModules();
// Generate an array of arrays with a dummy data
const generateData = (rows = 3, columns = 7, additionalRows = true) => {
let counter = 0;
const array2d = [...new Array(rows)].map((_) => [...new Array(columns)].map((_) => counter++));
if (additionalRows) {
array2d.push([]);
array2d.push([]);
}
return array2d;
};
const hotSettings = ref<GridSettings>({
data: generateData(),
colHeaders: true,
rowHeaders: true,
height: 'auto',
contextMenu: true,
manualRowMove: true,
bindRowsWithHeaders: true,
autoWrapRow: true,
autoWrapCol: true,
licenseKey: 'non-commercial-and-evaluation',
});
</script>
<template>
<div id="example1">
<HotTable :settings="hotSettings" />
</div>
</template>

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 page.

Related guides

Configuration options

Core methods

Hooks

Plugins