Skip to content

Sheets bar

Render a tab bar above or below the grid and let users switch between the sheets of a multi-sheet workbook.

Overview

The SheetsBar plugin renders a tab bar below the grid, or above it with the position option. Each tab represents one sheet, and clicking a tab switches the grid to that sheet’s data. Every sheet owns its own data, its own configuration overrides, and its own runtime view state, such as scroll position and selection.

The plugin is disabled by default. Enable it with the sheetsBar option.

Enable the sheets bar

Set sheetsBar to true to wrap the grid’s existing data into a single sheet named Sheet1, or pass an object with a sheets array to define the workbook up front.

JavaScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
// Register all Handsontable's modules.
registerAllModules();
const container = document.querySelector('#example');
new Handsontable(container, {
sheetsBar: {
sheets: [
{
name: 'Budget',
data: [
['Marketing', 4200, 5100],
['Engineering', 18700, 19200],
['Operations', 3100, 2900],
],
settings: {
colHeaders: ['Category • A', 'Q1 2026 • B', 'Q2 2026 • C'],
},
},
{
name: 'Notes',
data: [
['Reviewed by Ana García on 2026-03-14'],
['Pending sign-off from Finance'],
],
settings: {
colHeaders: ['Note • A'],
},
},
],
activeSheet: 0,
},
rowHeaders: true,
colHeaders: true,
stretchH: 'all',
height: 240,
licenseKey: 'non-commercial-and-evaluation',
});
TypeScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
// Register all Handsontable's modules.
registerAllModules();
const container = document.querySelector('#example')!;
new Handsontable(container, {
sheetsBar: {
sheets: [
{
name: 'Budget',
data: [
['Marketing', 4200, 5100],
['Engineering', 18700, 19200],
['Operations', 3100, 2900],
],
settings: {
colHeaders: ['Category • A', 'Q1 2026 • B', 'Q2 2026 • C'],
},
},
{
name: 'Notes',
data: [
['Reviewed by Ana García on 2026-03-14'],
['Pending sign-off from Finance'],
],
settings: {
colHeaders: ['Note • A'],
},
},
],
activeSheet: 0,
},
rowHeaders: true,
colHeaders: true,
stretchH: 'all',
height: 240,
licenseKey: 'non-commercial-and-evaluation',
});

Click the Budget and Notes tabs to switch between the two sheets. Click the + control to add a new sheet.

Configuration options

You can customize the sheets bar using available settings, such as defining the initial workbook, choosing the sheet that opens first, or showing and hiding the bar’s controls.

Each entry in sheets describes one sheet: its tab label, its source data, and optional grid settings applied while that sheet is active. Settings a sheet does not declare keep their grid-level values.

You can configure the following options:

const configurationOptions = {
sheetsBar: {
// Define the initial workbook. When omitted, the grid's
// own data becomes a single sheet named `Sheet1`.
sheets: [
{
// Set the sheet's tab label. When omitted, the sheet gets
// the next free `Sheet{n}` name.
name: 'Budget',
// Provide the sheet's source data
data: [['Item', 'Cost'], ['Rent', 1200]],
// Apply grid settings while this sheet is active
// (for example, `columns` or `colWidths`)
settings: { colWidths: 120 },
},
{ name: 'Notes', data: [['Draft']] },
],
// Set the index (within `sheets`) of the sheet to activate
// on initialization. Passed later through `updateSettings()`
// with a changed value, it switches the active sheet without
// rebuilding the workbook
activeSheet: 0,
// Show or hide the add-sheet and all-sheets menu controls
controls: true,
// Show or hide the tab-scrolling arrows that appear when tabs
// overflow the bar's width
paging: true,
// Render the bar above or below the grid
position: 'bottom',
// Custom container where the sheets bar UI will be injected
// (optional). When omitted, the bar renders in the layout slot
// the `position` option names.
uiContainer: null,
}
};

Read more about where the bar renders in the layout slots guide.

Managing sheets programmatically

Get the plugin instance with getPlugin() and call its API methods.

const sheetsBar = hot.getPlugin('sheetsBar');
// list every sheet, in tab order
sheetsBar.getSheets();
// [{ id: 1, name: 'Budget', isActive: true }, { id: 2, name: 'Notes', isActive: false }]
// switch the active sheet, by id or by name
sheetsBar.setActiveSheet('Notes');
// append a new sheet
sheetsBar.addSheet('Q3 Forecast', [['Category', 'Q3 2026']]);
// rename, copy, or remove a sheet, by id
sheetsBar.renameSheet(2, 'Comments');
sheetsBar.duplicateSheet(1);
sheetsBar.removeSheet(2);

getSheets() returns a SheetDescriptor for each sheet: its id, its name, and whether it is the active sheet. setActiveSheet() and addSheet() return false (or null for addSheet()) when the operation is rejected — for example, when a beforeSheetTabChange listener cancels a switch. moveSheetToIndex(id, index) moves a sheet to a new tab-order index and returns false when the move is rejected. renameSheet(id, name) and removeSheet(id) return false when the rename or removal is rejected, and duplicateSheet(id) returns a SheetDescriptor for the copy, or null when the copy is rejected.

Tab menus

Each tab has a menu with Rename, Duplicate, Delete, Move left, and Move right actions. Open it with the chevron on the active tab, or right-click any tab. On an inactive tab, the chevron first switches to that sheet — click it again to open the menu. Double-click a tab to rename it inline.

The all-sheets menu — opened from the control next to the add-sheet button — lists every sheet and lets you jump to any of them, including sheets that are scrolled out of view.

You can also reorder sheets by dragging a tab along the bar. Deleting the active sheet activates its nearest remaining neighbor, and the last remaining sheet can’t be deleted.

Overflow paging

When the tabs no longer fit the bar’s width, paging arrows appear at the end of the strip. Click an arrow to scroll the tabs by one page. Set paging: false to hide the arrows and let the tab strip overflow instead.

Right-to-left layout

Under a right-to-left layout, the tab strip mirrors: the first sheet’s tab renders on the right, and the paging arrows swap direction. The tab menu’s Move left and Move right actions keep referring to the visual direction, not the underlying array order.

Use sheets bar hooks

You can run your code before or after different sheet operations, using the following Handsontable hooks. Every before hook can cancel its operation by returning false.

const configurationOptions = {
beforeSheetTabChange(oldSheetId, newSheetId) {
// add your code here
return false; // to block the sheet switch
},
afterSheetTabChange(oldSheetId, newSheetId) {
// add your code here
},
beforeSheetTabRemove(sheetId) {
// add your code here
return false; // to block the removal
},
// ...
};

Per-sheet view state

Switching sheets captures the outgoing sheet’s scroll position, selection, sort, filters, hidden and trimmed indexes, merged cells, custom borders, manual column widths and row heights, and any cell-meta changes you made while it was active. It restores that state the next time you switch back to it. A sheet you have not visited yet opens with a neutral view state, so it never inherits the previous sheet’s filters or sizes. The afterSheetTabStateCapture and afterSheetTabStateRestore hooks fire after each capture and restore.

Use formulas across sheets

A sheet can enable the Formulas plugin through its own settings, and its formulas recalculate within that sheet.

Formulas can also reference other sheets of the workbook. Share one HyperFormula instance across the sheets, and give each sheet its own sheetName within that instance:

const engine = HyperFormula.buildEmpty({
licenseKey: 'internal-use-in-handsontable',
});
const configurationOptions = {
sheetsBar: {
sheets: [
{
name: 'Rates',
data: [[0.23]],
settings: { formulas: { engine, sheetName: 'Rates' } },
},
{
name: 'Budget',
data: [[100, '=A1*Rates!A1']],
settings: { formulas: { engine, sheetName: 'Budget' } },
},
],
},
};

Editing a value on one sheet recalculates the formulas that reference it on the other sheets. In the demo below, switch to the Rates sheet, change the VAT rate, and return to Budget: the VAT and gross amounts recalculate.

JavaScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
import { HyperFormula } from 'hyperformula';
// Register all Handsontable's modules.
registerAllModules();
// One engine, shared by every sheet of the workbook.
const engine = HyperFormula.buildEmpty({
licenseKey: 'internal-use-in-handsontable',
});
const ratesData = [
['VAT rate', 0.23],
['Service fee', 0.05],
];
const container = document.querySelector('#example2');
new Handsontable(container, {
sheetsBar: {
sheets: [
{
name: 'Budget',
data: [
['Laptops', 4200, '=B1*Rates!B1', '=B1*Rates!B2', '=B1+C1+D1'],
['Desks', 1200, '=B2*Rates!B1', '=B2*Rates!B2', '=B2+C2+D2'],
['Monitors', 2600, '=B3*Rates!B1', '=B3*Rates!B2', '=B3+C3+D3'],
['Chairs', 950, '=B4*Rates!B1', '=B4*Rates!B2', '=B4+C4+D4'],
['Docking stations', 780, '=B5*Rates!B1', '=B5*Rates!B2', '=B5+C5+D5'],
['Total', '=SUM(B1:B5)', '=SUM(C1:C5)', '=SUM(D1:D5)', '=SUM(E1:E5)'],
],
settings: {
colHeaders: ['Item • A', 'Net • B', 'VAT • C', 'Fee • D', 'Gross • E'],
formulas: { engine, sheetName: 'Budget' },
},
},
{
name: 'Rates',
data: ratesData,
settings: {
colHeaders: ['Rate • A', 'Value • B'],
formulas: { engine, sheetName: 'Rates' },
},
},
],
activeSheet: 0,
},
rowHeaders: true,
colHeaders: true,
stretchH: 'all',
height: 240,
licenseKey: 'non-commercial-and-evaluation',
});
TypeScript
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
import { HyperFormula } from 'hyperformula';
// Register all Handsontable's modules.
registerAllModules();
// One engine, shared by every sheet of the workbook.
const engine = HyperFormula.buildEmpty({
licenseKey: 'internal-use-in-handsontable',
});
const ratesData = [
['VAT rate', 0.23],
['Service fee', 0.05],
];
const container = document.querySelector('#example2')!;
new Handsontable(container, {
sheetsBar: {
sheets: [
{
name: 'Budget',
data: [
['Laptops', 4200, '=B1*Rates!B1', '=B1*Rates!B2', '=B1+C1+D1'],
['Desks', 1200, '=B2*Rates!B1', '=B2*Rates!B2', '=B2+C2+D2'],
['Monitors', 2600, '=B3*Rates!B1', '=B3*Rates!B2', '=B3+C3+D3'],
['Chairs', 950, '=B4*Rates!B1', '=B4*Rates!B2', '=B4+C4+D4'],
['Docking stations', 780, '=B5*Rates!B1', '=B5*Rates!B2', '=B5+C5+D5'],
['Total', '=SUM(B1:B5)', '=SUM(C1:C5)', '=SUM(D1:D5)', '=SUM(E1:E5)'],
],
settings: {
colHeaders: ['Item • A', 'Net • B', 'VAT • C', 'Fee • D', 'Gross • E'],
formulas: { engine, sheetName: 'Budget' },
},
},
{
name: 'Rates',
data: ratesData,
settings: {
colHeaders: ['Rate • A', 'Value • B'],
formulas: { engine, sheetName: 'Rates' },
},
},
],
activeSheet: 0,
},
rowHeaders: true,
colHeaders: true,
stretchH: 'all',
height: 240,
licenseKey: 'non-commercial-and-evaluation',
});

One thing to keep in mind: pass a built engine instance (HyperFormula.buildEmpty()), not the HyperFormula class. The class builds a separate engine for each sheet, and separate engines can’t see each other’s data. The plugin warns in the console when a sheetName rides on the class.

A sheet added at runtime — through the bar’s add button, or through addSheet() without a settings argument — joins the workbook’s shared engine under its own name, so formulas work on it and cross-sheet references can reach it right away. Pass a settings argument to addSheet() to configure the new sheet yourself.

Known limitations

  • Only one sheet’s data and settings are loaded into the grid at a time.
  • Passing a changed sheets value through updateSettings() rebuilds the whole workbook, discarding any sheets you added at runtime with addSheet(). A re-passed value counts as unchanged when each sheet’s settings are structurally equal and each sheet’s data is the same array reference. A framework wrapper that re-emits a fresh settings literal on every render therefore keeps the workbook, as long as the data arrays are stable references - inline data literals recreated on each render reset it. A rebuild keeps the active sheet selected by name when the new workbook still contains it and the activeSheet option did not change. Changing only the bar’s UI options (controls, paging, position) never rebuilds the workbook.
  • Switching sheets calls loadData() internally, which clears the UndoRedo plugin’s undo and redo stacks.
  • Cell meta set with setCellMeta() survives a switch round trip, except the valid flag - validation results are recomputed by the next validation rather than restored.
  • The bar shares its container with the grid. When the window scrolls the grid (no height option and no scrollable ancestor), the bar follows the last row, so on a long sheet you scroll past the data to reach it. To keep the bar in view, give the grid an explicit height, or put it in a container with a fixed height and overflow: auto. The Pagination bar behaves the same way.

Each tab is a single stop in the tab order.

WindowsmacOSAction
Enter / SpaceEnter / SpaceSwitch to the focused tab’s sheet. On the active tab, open the tab’s menu
Arrow keysArrow keysMove the focus along the tab strip
Home / EndHome / EndMove the focus to the first or last tab
EscapeEscapeClose the open menu, cancel an inline rename, or cancel a drag in progress

Use the menu’s Move left and Move right actions to reorder sheets from the keyboard, and its Rename action to rename a sheet.

Configuration options

Hooks

Plugins